From 93b398a54feea3fd5f934a656f2e99d4aadc18d4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 18 Apr 2022 18:22:00 +0400 Subject: [PATCH 01/51] 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(); From 948f905a663e75dcc44744e75b45b3dbf6533e32 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 19 Apr 2022 17:37:06 +0400 Subject: [PATCH 02/51] Store network stats --- lightning-backend/src/index.ts | 4 +- lightning-backend/src/tasks/stats-updater.ts | 62 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 lightning-backend/src/tasks/stats-updater.ts diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index 23bd56cb7..f2dc0fb91 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -2,6 +2,7 @@ import config from './config'; import logger from './logger'; import DB from './database'; import lightningApi from './api/lightning-api-factory'; +import statsUpdater from './tasks/stats-updater'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); @@ -13,8 +14,7 @@ class LightningServer { async init() { await DB.checkDbConnection(); - const networkInfo = await lightningApi.getNetworkInfo(); - logger.info(JSON.stringify(networkInfo)); + statsUpdater.startService(); const networkGraph = await lightningApi.getNetworkGraph(); logger.info('Network graph channels: ' + networkGraph.channels.length); diff --git a/lightning-backend/src/tasks/stats-updater.ts b/lightning-backend/src/tasks/stats-updater.ts new file mode 100644 index 000000000..251e30b15 --- /dev/null +++ b/lightning-backend/src/tasks/stats-updater.ts @@ -0,0 +1,62 @@ + +import DB from '../database'; +import logger from '../logger'; +import lightningApi from '../api/lightning-api-factory'; + +/* +CREATE TABLE IF NOT EXISTS lightning_stats ( + id int(11) NOT NULL AUTO_INCREMENT, + added datetime NOT NULL, + channel_count int(11) NOT NULL, + node_count int(11) NOT NULL, + total_capacity double unsigned NOT NULL, + average_channel_size double unsigned NOT NULL, + CONSTRAINT PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +*/ + + +class LightningStatsUpdater { + constructor() {} + + public async startService() { + logger.info('Starting Lightning Stats service'); + + 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(() => { + this.$logLightningStats(); + setInterval(() => { + this.$logLightningStats(); + }, 1000 * 60 * 60); + }, difference); + } + + private async $logLightningStats() { + const networkInfo = await lightningApi.getNetworkInfo(); + + try { + const query = `INSERT INTO lightning_stats( + added, + channel_count, + node_count, + total_capacity, + average_channel_size + ) + VALUES (NOW(), ?, ?, ?, ?)`; + + await DB.query(query, [ + networkInfo.channel_count, + networkInfo.node_count, + networkInfo.total_capacity, + networkInfo.average_channel_size + ]); + } catch (e) { + logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e)); + } + } +} + +export default new LightningStatsUpdater(); From 3e6af8e87badde88b7b97edeabbca729ff2e1636 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 24 Apr 2022 01:33:38 +0400 Subject: [PATCH 03/51] Store nodes and channels --- lightning-backend/mempool-config.sample.json | 6 +- .../src/api/lightning-api-abstract-factory.ts | 5 +- .../src/api/lightning-api.interface.ts | 7 + lightning-backend/src/api/lnd/lnd-api.ts | 8 +- lightning-backend/src/database-migration.ts | 232 ++++++++++++++++++ lightning-backend/src/index.ts | 11 +- .../src/tasks/node-sync.service.ts | 163 ++++++++++++ ...ts-updater.ts => stats-updater.service.ts} | 19 +- 8 files changed, 422 insertions(+), 29 deletions(-) create mode 100644 lightning-backend/src/database-migration.ts create mode 100644 lightning-backend/src/tasks/node-sync.service.ts rename lightning-backend/src/tasks/{stats-updater.ts => stats-updater.service.ts} (69%) diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index 8df468cc6..e45742042 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -20,8 +20,8 @@ "HOST": "127.0.0.1", "PORT": 3306, "SOCKET": "/var/run/mysql/mysql.sock", - "DATABASE": "mempool", - "USERNAME": "mempool", - "PASSWORD": "mempool" + "DATABASE": "lightning", + "USERNAME": "root", + "PASSWORD": "root" } } diff --git a/lightning-backend/src/api/lightning-api-abstract-factory.ts b/lightning-backend/src/api/lightning-api-abstract-factory.ts index 086498dd4..2e78b52f8 100644 --- a/lightning-backend/src/api/lightning-api-abstract-factory.ts +++ b/lightning-backend/src/api/lightning-api-abstract-factory.ts @@ -1,6 +1,7 @@ import { ILightningApi } from './lightning-api.interface'; export interface AbstractLightningApi { - getNetworkInfo(): Promise; - getNetworkGraph(): Promise; + $getNetworkInfo(): Promise; + $getNetworkGraph(): Promise; + $getChanInfo(id: string): Promise; } diff --git a/lightning-backend/src/api/lightning-api.interface.ts b/lightning-backend/src/api/lightning-api.interface.ts index 4540185ff..26999c119 100644 --- a/lightning-backend/src/api/lightning-api.interface.ts +++ b/lightning-backend/src/api/lightning-api.interface.ts @@ -26,6 +26,13 @@ export namespace ILightningApi { interface Policy { public_key: string; + base_fee_mtokens?: number; + cltv_delta?: number; + fee_rate?: number; + is_disabled?: boolean; + max_htlc_mtokens?: number; + min_htlc_mtokens?: number; + updated_at?: string; } export interface Node { diff --git a/lightning-backend/src/api/lnd/lnd-api.ts b/lightning-backend/src/api/lnd/lnd-api.ts index ebf477ca4..edddc31c6 100644 --- a/lightning-backend/src/api/lnd/lnd-api.ts +++ b/lightning-backend/src/api/lnd/lnd-api.ts @@ -25,13 +25,17 @@ class LndApi implements AbstractLightningApi { } } - async getNetworkInfo(): Promise { + async $getNetworkInfo(): Promise { return await lnService.getNetworkInfo({ lnd: this.lnd }); } - async getNetworkGraph(): Promise { + async $getNetworkGraph(): Promise { return await lnService.getNetworkGraph({ lnd: this.lnd }); } + + async $getChanInfo(id: string): Promise { + return await lnService.getChannel({ lnd: this.lnd, id }); + } } export default LndApi; diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts new file mode 100644 index 000000000..4690fa0e0 --- /dev/null +++ b/lightning-backend/src/database-migration.ts @@ -0,0 +1,232 @@ +import config from './config'; +import DB from './database'; +import logger from './logger'; + +const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); + +class DatabaseMigration { + private static currentVersion = 1; + private queryTimeout = 120000; + + constructor() { } + /** + * Entry point + */ + public async $initializeOrMigrateDatabase(): Promise { + logger.debug('MIGRATIONS: Running migrations'); + + await this.$printDatabaseVersion(); + + // First of all, if the `state` database does not exist, create it so we can track migration version + if (!await this.$checkIfTableExists('state')) { + logger.debug('MIGRATIONS: `state` table does not exist. Creating it.'); + try { + await this.$createMigrationStateTable(); + } catch (e) { + logger.err('MIGRATIONS: Unable to create `state` table, aborting in 10 seconds. ' + e); + await sleep(10000); + process.exit(-1); + } + logger.debug('MIGRATIONS: `state` table initialized.'); + } + + let databaseSchemaVersion = 0; + try { + databaseSchemaVersion = await this.$getSchemaVersionFromDatabase(); + } catch (e) { + logger.err('MIGRATIONS: Unable to get current database migration version, aborting in 10 seconds. ' + e); + await sleep(10000); + process.exit(-1); + } + + logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion); + logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion); + if (databaseSchemaVersion >= DatabaseMigration.currentVersion) { + logger.debug('MIGRATIONS: Nothing to do.'); + return; + } + + // Now, create missing tables. Those queries cannot be wrapped into a transaction unfortunately + try { + await this.$createMissingTablesAndIndexes(databaseSchemaVersion); + } catch (e) { + logger.err('MIGRATIONS: Unable to create required tables, aborting in 10 seconds. ' + e); + await sleep(10000); + process.exit(-1); + } + + if (DatabaseMigration.currentVersion > databaseSchemaVersion) { + logger.notice('MIGRATIONS: Upgrading datababse schema'); + try { + await this.$migrateTableSchemaFromVersion(databaseSchemaVersion); + logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`); + } catch (e) { + logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e); + } + } + + return; + } + + /** + * Create all missing tables + */ + private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) { + try { + await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); + await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); + await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); + } catch (e) { + throw e; + } + } + + /** + * Small query execution wrapper to log all executed queries + */ + private async $executeQuery(query: string, silent: boolean = false): Promise { + if (!silent) { + logger.debug('MIGRATIONS: Execute query:\n' + query); + } + return DB.query({ sql: query, timeout: this.queryTimeout }); + } + + /** + * Check if 'table' exists in the database + */ + private async $checkIfTableExists(table: string): Promise { + const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`; + const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); + return rows[0]['COUNT(*)'] === 1; + } + + /** + * Get current database version + */ + private async $getSchemaVersionFromDatabase(): Promise { + const query = `SELECT number FROM state WHERE name = 'schema_version';`; + const [rows] = await this.$executeQuery(query, true); + return rows[0]['number']; + } + + /** + * Create the `state` table + */ + private async $createMigrationStateTable(): Promise { + try { + const query = `CREATE TABLE IF NOT EXISTS state ( + name varchar(25) NOT NULL, + number int(11) NULL, + string varchar(100) NULL, + CONSTRAINT name_unique UNIQUE (name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + await this.$executeQuery(query); + + // Set initial values + await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`); + } catch (e) { + throw e; + } + } + + /** + * We actually execute the migrations queries here + */ + private async $migrateTableSchemaFromVersion(version: number): Promise { + const transactionQueries: string[] = []; + for (const query of this.getMigrationQueriesFromVersion(version)) { + transactionQueries.push(query); + } + transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery()); + + try { + await this.$executeQuery('START TRANSACTION;'); + for (const query of transactionQueries) { + await this.$executeQuery(query); + } + await this.$executeQuery('COMMIT;'); + } catch (e) { + await this.$executeQuery('ROLLBACK;'); + throw e; + } + } + + /** + * Generate migration queries based on schema version + */ + private getMigrationQueriesFromVersion(version: number): string[] { + const queries: string[] = []; + return queries; + } + + /** + * Save the schema version in the database + */ + private getUpdateToLatestSchemaVersionQuery(): string { + return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`; + } + + /** + * Print current database version + */ + private async $printDatabaseVersion() { + try { + const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true); + logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`); + } catch (e) { + logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e); + } + } + + private getCreateStatisticsQuery(): string { + return `CREATE TABLE IF NOT EXISTS statistics ( + id int(11) NOT NULL AUTO_INCREMENT, + added datetime NOT NULL, + channel_count int(11) NOT NULL, + node_count int(11) NOT NULL, + total_capacity double unsigned NOT NULL, + average_channel_size double unsigned NOT NULL, + CONSTRAINT PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateNodesQuery(): string { + return `CREATE TABLE IF NOT EXISTS nodes ( + public_key varchar(66) NOT NULL, + first_seen datetime NOT NULL, + updated_at datetime NOT NULL, + alias varchar(200) COLLATE utf8mb4_general_ci NOT NULL, + color varchar(200) NOT NULL, + CONSTRAINT PRIMARY KEY (public_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateChannelsQuery(): string { + return `CREATE TABLE IF NOT EXISTS channels ( + id varchar(15) NOT NULL, + capacity double unsigned NOT NULL, + transaction_id varchar(64) NOT NULL, + transaction_vout int(11) NOT NULL, + updated_at datetime NOT NULL, + node1_public_key varchar(66) NOT NULL, + node1_base_fee_mtokens double unsigned NULL, + node1_cltv_delta int(11) NULL, + node1_fee_rate int(11) NULL, + node1_is_disabled boolean NULL, + node1_max_htlc_mtokens double unsigned NULL, + node1_min_htlc_mtokens double unsigned NULL, + node1_updated_at datetime NULL, + node2_public_key varchar(66) NOT NULL, + node2_base_fee_mtokens double unsigned NULL, + node2_cltv_delta int(11) NULL, + node2_fee_rate int(11) NULL, + node2_is_disabled boolean NULL, + node2_max_htlc_mtokens double unsigned NULL, + node2_min_htlc_mtokens double unsigned NULL, + node2_updated_at datetime NULL, + CONSTRAINT PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } +} + +export default new DatabaseMigration(); diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index f2dc0fb91..a9e487ef5 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -1,8 +1,9 @@ import config from './config'; import logger from './logger'; import DB from './database'; -import lightningApi from './api/lightning-api-factory'; -import statsUpdater from './tasks/stats-updater'; +import databaseMigration from './database-migration'; +import statsUpdater from './tasks/stats-updater.service'; +import nodeSyncService from './tasks/node-sync.service'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); @@ -13,12 +14,10 @@ class LightningServer { async init() { await DB.checkDbConnection(); + await databaseMigration.$initializeOrMigrateDatabase(); statsUpdater.startService(); - - const networkGraph = await lightningApi.getNetworkGraph(); - logger.info('Network graph channels: ' + networkGraph.channels.length); - logger.info('Network graph nodes: ' + networkGraph.nodes.length); + nodeSyncService.startService(); } } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts new file mode 100644 index 000000000..8b5116be9 --- /dev/null +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -0,0 +1,163 @@ + +import DB from '../database'; +import logger from '../logger'; +import lightningApi from '../api/lightning-api-factory'; +import { ILightningApi } from '../api/lightning-api.interface'; + +class NodeSyncService { + constructor() {} + + public async startService() { + logger.info('Starting node sync service'); + + this.$updateNodes(); + + setInterval(async () => { + await this.$updateNodes(); + }, 1000 * 60 * 60); + } + + private async $updateNodes() { + try { + const networkGraph = await lightningApi.$getNetworkGraph(); + + for (const node of networkGraph.nodes) { + await this.$saveNode(node); + } + + for (const channel of networkGraph.channels) { + await this.$saveChannel(channel); + } + } catch (e) { + logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveChannel(channel: ILightningApi.Channel) { + try { + const d = new Date(Date.parse(channel.updated_at)); + const query = `INSERT INTO channels + ( + id, + capacity, + transaction_id, + transaction_vout, + updated_at, + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + capacity = ?, + updated_at = ?, + 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, [ + channel.id, + channel.capacity, + channel.transaction_id, + channel.transaction_vout, + channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0, + channel.policies[0].public_key, + channel.policies[0].base_fee_mtokens, + channel.policies[0].cltv_delta, + channel.policies[0].fee_rate, + channel.policies[0].is_disabled, + channel.policies[0].max_htlc_mtokens, + channel.policies[0].min_htlc_mtokens, + channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0, + channel.policies[1].public_key, + channel.policies[1].base_fee_mtokens, + channel.policies[1].cltv_delta, + channel.policies[1].fee_rate, + channel.policies[1].is_disabled, + channel.policies[1].max_htlc_mtokens, + channel.policies[1].min_htlc_mtokens, + channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0, + channel.capacity, + channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0, + channel.policies[0].public_key, + channel.policies[0].base_fee_mtokens, + channel.policies[0].cltv_delta, + channel.policies[0].fee_rate, + channel.policies[0].is_disabled, + channel.policies[0].max_htlc_mtokens, + channel.policies[0].min_htlc_mtokens, + channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0, + channel.policies[1].public_key, + channel.policies[1].base_fee_mtokens, + channel.policies[1].cltv_delta, + channel.policies[1].fee_rate, + channel.policies[1].is_disabled, + channel.policies[1].max_htlc_mtokens, + channel.policies[1].min_htlc_mtokens, + channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0, + ]); + } catch (e) { + logger.err('$saveChannel() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveNode(node: ILightningApi.Node) { + try { + const updatedAt = this.utcDateToMysql(node.updated_at); + const query = `INSERT INTO nodes( + public_key, + first_seen, + updated_at, + alias, + color + ) + VALUES (?, NOW(), ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?;`; + + await DB.query(query, [ + node.public_key, + updatedAt, + node.alias, + node.color, + updatedAt, + node.alias, + node.color, + ]); + } catch (e) { + logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private utcDateToMysql(dateString: string): string { + const d = new Date(Date.parse(dateString)); + return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0]; + } +} + +export default new NodeSyncService(); diff --git a/lightning-backend/src/tasks/stats-updater.ts b/lightning-backend/src/tasks/stats-updater.service.ts similarity index 69% rename from lightning-backend/src/tasks/stats-updater.ts rename to lightning-backend/src/tasks/stats-updater.service.ts index 251e30b15..2946977bc 100644 --- a/lightning-backend/src/tasks/stats-updater.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -3,24 +3,11 @@ import DB from '../database'; import logger from '../logger'; import lightningApi from '../api/lightning-api-factory'; -/* -CREATE TABLE IF NOT EXISTS lightning_stats ( - id int(11) NOT NULL AUTO_INCREMENT, - added datetime NOT NULL, - channel_count int(11) NOT NULL, - node_count int(11) NOT NULL, - total_capacity double unsigned NOT NULL, - average_channel_size double unsigned NOT NULL, - CONSTRAINT PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -*/ - - class LightningStatsUpdater { constructor() {} public async startService() { - logger.info('Starting Lightning Stats service'); + logger.info('Starting Stats service'); const now = new Date(); const nextHourInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), Math.floor(now.getHours() / 1) + 1, 0, 0, 0); @@ -35,10 +22,10 @@ class LightningStatsUpdater { } private async $logLightningStats() { - const networkInfo = await lightningApi.getNetworkInfo(); + const networkInfo = await lightningApi.$getNetworkInfo(); try { - const query = `INSERT INTO lightning_stats( + const query = `INSERT INTO statistics( added, channel_count, node_count, From 8d622e3606b038a04b74dddc9e81089320e706f5 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 27 Apr 2022 02:52:23 +0400 Subject: [PATCH 04/51] Store and display stats and node top lists --- frontend/mempool-frontend-config.sample.json | 3 +- frontend/proxy.conf.local.js | 10 +++ frontend/src/app/app-routing.module.ts | 16 ++++ .../components/change/change.component.html | 3 + .../components/change/change.component.scss | 0 .../app/components/change/change.component.ts | 21 +++++ .../master-page/master-page.component.html | 6 +- .../app/lightning/lightning-api.service.ts | 22 +++++ .../lightning-dashboard.component.html | 48 +++++++++++ .../lightning-dashboard.component.scss | 80 +++++++++++++++++ .../lightning-dashboard.component.ts | 37 ++++++++ .../src/app/lightning/lightning.module.ts | 24 ++++++ .../node-statistics.component.html | 64 ++++++++++++++ .../node-statistics.component.scss | 85 +++++++++++++++++++ .../node-statistics.component.ts | 18 ++++ .../nodes-list/nodes-list.component.html | 39 +++++++++ .../nodes-list/nodes-list.component.scss | 0 .../nodes-list/nodes-list.component.ts | 18 ++++ frontend/src/app/services/state.service.ts | 4 +- frontend/src/app/shared/shared.module.ts | 7 +- lightning-backend/mempool-config.sample.json | 3 +- .../lightning-api-abstract-factory.ts | 0 .../{ => lightning}/lightning-api-factory.ts | 2 +- .../lightning-api.interface.ts | 0 .../src/api/{ => lightning}/lnd/lnd-api.ts | 4 +- lightning-backend/src/api/nodes/nodes.api.ts | 42 +++++++++ .../src/api/nodes/nodes.routes.ts | 35 ++++++++ lightning-backend/src/config.ts | 2 + lightning-backend/src/database-migration.ts | 54 ++++++++---- lightning-backend/src/index.ts | 28 ++++++ .../src/tasks/node-sync.service.ts | 4 +- .../src/tasks/stats-updater.service.ts | 19 ++++- 32 files changed, 663 insertions(+), 35 deletions(-) create mode 100644 frontend/src/app/components/change/change.component.html create mode 100644 frontend/src/app/components/change/change.component.scss create mode 100644 frontend/src/app/components/change/change.component.ts create mode 100644 frontend/src/app/lightning/lightning-api.service.ts create mode 100644 frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html create mode 100644 frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss create mode 100644 frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts create mode 100644 frontend/src/app/lightning/lightning.module.ts create mode 100644 frontend/src/app/lightning/node-statistics/node-statistics.component.html create mode 100644 frontend/src/app/lightning/node-statistics/node-statistics.component.scss create mode 100644 frontend/src/app/lightning/node-statistics/node-statistics.component.ts create mode 100644 frontend/src/app/lightning/nodes-list/nodes-list.component.html create mode 100644 frontend/src/app/lightning/nodes-list/nodes-list.component.scss create mode 100644 frontend/src/app/lightning/nodes-list/nodes-list.component.ts rename lightning-backend/src/api/{ => lightning}/lightning-api-abstract-factory.ts (100%) rename lightning-backend/src/api/{ => lightning}/lightning-api-factory.ts (89%) rename lightning-backend/src/api/{ => lightning}/lightning-api.interface.ts (100%) rename lightning-backend/src/api/{ => lightning}/lnd/lnd-api.ts (94%) create mode 100644 lightning-backend/src/api/nodes/nodes.api.ts create mode 100644 lightning-backend/src/api/nodes/nodes.routes.ts diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index 231f1c7c8..938c71c1b 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -16,5 +16,6 @@ "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", - "MINING_DASHBOARD": true + "MINING_DASHBOARD": true, + "LIGHTNING": false } diff --git a/frontend/proxy.conf.local.js b/frontend/proxy.conf.local.js index b1bf0656d..9be0ed770 100644 --- a/frontend/proxy.conf.local.js +++ b/frontend/proxy.conf.local.js @@ -102,6 +102,16 @@ if (configContent && configContent.BASE_MODULE === 'bisq') { } PROXY_CONFIG.push(...[ + { + context: ['/lightning/api/v1/**'], + target: `http://localhost:8899`, + secure: false, + changeOrigin: true, + proxyTimeout: 30000, + pathRewrite: { + "^/lightning/api": "/api" + }, + }, { context: ['/api/v1/**'], target: `http://localhost:8999`, diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index b62f586a4..dfcc7aec7 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -20,6 +20,7 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass import { AssetsComponent } from './components/assets/assets.component'; import { AssetComponent } from './components/asset/asset.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; +import { LightningDashboardComponent } from './lightning/lightning-dashboard/lightning-dashboard.component'; let routes: Routes = [ { @@ -96,6 +97,11 @@ let routes: Routes = [ path: 'api', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) }, + { + path: 'lightning', + component: LightningDashboardComponent, + loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) + }, ], }, { @@ -186,6 +192,11 @@ let routes: Routes = [ path: 'api', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) }, + { + path: 'lightning', + component: LightningDashboardComponent, + loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) + }, ], }, { @@ -273,6 +284,11 @@ let routes: Routes = [ path: 'api', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) }, + { + path: 'lightning', + component: LightningDashboardComponent, + loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) + }, ], }, { diff --git a/frontend/src/app/components/change/change.component.html b/frontend/src/app/components/change/change.component.html new file mode 100644 index 000000000..117a0c534 --- /dev/null +++ b/frontend/src/app/components/change/change.component.html @@ -0,0 +1,3 @@ + + {{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}% + diff --git a/frontend/src/app/components/change/change.component.scss b/frontend/src/app/components/change/change.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/change/change.component.ts b/frontend/src/app/components/change/change.component.ts new file mode 100644 index 000000000..1fba853c9 --- /dev/null +++ b/frontend/src/app/components/change/change.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-change', + templateUrl: './change.component.html', + styleUrls: ['./change.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChangeComponent implements OnChanges { + @Input() current: number; + @Input() previous: number; + + change: number; + + constructor() { } + + ngOnChanges(): void { + this.change = (this.current - this.previous) / this.previous * 100; + } + +} diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 59d82f128..bef1dba8c 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -35,15 +35,15 @@ + - diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts new file mode 100644 index 000000000..3f36b55cd --- /dev/null +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +const API_BASE_URL = '/lightning/api/v1'; + +@Injectable({ + providedIn: 'root' +}) +export class LightningApiService { + constructor( + private httpClient: HttpClient, + ) { } + + getLatestStatistics$(): Observable { + return this.httpClient.get(API_BASE_URL + '/statistics/latest'); + } + + listTopNodes$(): Observable { + return this.httpClient.get(API_BASE_URL + '/nodes/top'); + } +} diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html new file mode 100644 index 000000000..3c14763a5 --- /dev/null +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -0,0 +1,48 @@ +
+ +
+ +
+
+ Nodes Statistics  +
+
+
+
+ +
+
+
+
+ +
+
+ Channels Statistics  +
+
+ +
+
+ +
+
+
+
Top Capacity Nodes
+ + +
+
+
+ +
+
+
+
Most Connected Nodes
+ + +
+
+
+ +
+
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss new file mode 100644 index 000000000..4fdadd57b --- /dev/null +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss @@ -0,0 +1,80 @@ +.dashboard-container { + padding-bottom: 60px; + text-align: center; + margin-top: 0.5rem; + @media (min-width: 992px) { + padding-bottom: 0px; + } + .col { + margin-bottom: 1.5rem; + } +} + +.card { + background-color: #1d1f31; +} + +.card-title { + font-size: 1rem; + color: #4a68b9; +} +.card-title > a { + color: #4a68b9; +} + +.card-body { + padding: 1.25rem 1rem 0.75rem 1rem; +} +.card-body.pool-ranking { + padding: 1.25rem 0.25rem 0.75rem 0.25rem; +} +.card-text { + font-size: 22px; +} + + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.more-padding { + padding: 18px; +} + +.card-wrapper { + .card { + height: auto !important; + } + .card-body { + display: flex; + flex: inherit; + text-align: center; + flex-direction: column; + justify-content: space-around; + padding: 22px 20px; + } +} + +.skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } +} + +.card-text { + font-size: 22px; +} diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts new file mode 100644 index 000000000..bb24d7a64 --- /dev/null +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-lightning-dashboard', + templateUrl: './lightning-dashboard.component.html', + styleUrls: ['./lightning-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LightningDashboardComponent implements OnInit { + nodesByCapacity$: Observable; + nodesByChannels$: Observable; + statistics$: Observable; + + constructor( + private lightningApiService: LightningApiService, + ) { } + + ngOnInit(): void { + const sharedObservable = this.lightningApiService.listTopNodes$().pipe(share()); + + this.nodesByCapacity$ = sharedObservable + .pipe( + map((object) => object.topByCapacity), + ); + + this.nodesByChannels$ = sharedObservable + .pipe( + map((object) => object.topByChannels), + ); + + this.statistics$ = this.lightningApiService.getLatestStatistics$(); + } + +} diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts new file mode 100644 index 000000000..d98c0c910 --- /dev/null +++ b/frontend/src/app/lightning/lightning.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; +import { LightningApiService } from './lightning-api.service'; +import { NodesListComponent } from './nodes-list/nodes-list.component'; +import { RouterModule } from '@angular/router'; +import { NodeStatisticsComponent } from './node-statistics/node-statistics.component'; +@NgModule({ + declarations: [ + LightningDashboardComponent, + NodesListComponent, + NodeStatisticsComponent, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule, + ], + providers: [ + LightningApiService, + ] +}) +export class LightningModule { } diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html new file mode 100644 index 000000000..4a3842673 --- /dev/null +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -0,0 +1,64 @@ +
+
+
+
Nodes
+
+
+ {{ statistics.latest.node_count | number }} +
+ + + +
+
+
+
Channels
+
+
+ {{ statistics.latest.channel_count | number }} +
+ + + +
+
+
+
Average Channel
+
+ + + + +
+
+
+
+ + +
+
+
Nodes
+
+
+
+
+
+
+
Channels
+
+
+
+
+
+
+
Average Channel
+
+
+
+
+
+
+
diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.scss b/frontend/src/app/lightning/node-statistics/node-statistics.component.scss new file mode 100644 index 000000000..acc4578f3 --- /dev/null +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.scss @@ -0,0 +1,85 @@ +.card-title { + color: #4a68b9; + font-size: 10px; + margin-bottom: 4px; + font-size: 1rem; +} + +.card-text { + font-size: 22px; + span { + font-size: 11px; + position: relative; + top: -2px; + display: inline-flex; + } + .green-color { + display: block; + } +} + +.fee-estimation-container { + display: flex; + justify-content: space-between; + @media (min-width: 376px) { + flex-direction: row; + } + .item { + max-width: 150px; + margin: 0; + width: -webkit-fill-available; + @media (min-width: 376px) { + margin: 0 auto 0px; + } + &:first-child{ + display: none; + @media (min-width: 485px) { + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + &:last-child { + margin-bottom: 0; + } + .card-text span { + color: #ffffff66; + font-size: 12px; + top: 0px; + } + .fee-text{ + border-bottom: 1px solid #ffffff1c; + width: fit-content; + margin: auto; + line-height: 1.45; + padding: 0px 2px; + } + .fiat { + display: block; + font-size: 14px !important; + } + } +} + +.loading-container { + min-height: 76px; +} + +.card-text { + .skeleton-loader { + width: 100%; + display: block; + &:first-child { + max-width: 90px; + margin: 15px auto 3px; + } + &:last-child { + margin: 10px auto 3px; + max-width: 55px; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts new file mode 100644 index 000000000..c42720427 --- /dev/null +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-node-statistics', + templateUrl: './node-statistics.component.html', + styleUrls: ['./node-statistics.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodeStatisticsComponent implements OnInit { + @Input() statistics$: Observable; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.html b/frontend/src/app/lightning/nodes-list/nodes-list.component.html new file mode 100644 index 000000000..441c2e0f1 --- /dev/null +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.html @@ -0,0 +1,39 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + +
AliasCapacityChannels
+ {{ node.alias }} + + + + {{ node.channels_left + node.channels_right | number }} +
+ + + + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.scss b/frontend/src/app/lightning/nodes-list/nodes-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.ts b/frontend/src/app/lightning/nodes-list/nodes-list.component.ts new file mode 100644 index 000000000..d6d05833e --- /dev/null +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-nodes-list', + templateUrl: './nodes-list.component.html', + styleUrls: ['./nodes-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodesListComponent implements OnInit { + @Input() nodes$: Observable; + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 73c0e905d..0d0b05556 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -37,6 +37,7 @@ export interface Env { LIQUID_WEBSITE_URL: string; BISQ_WEBSITE_URL: string; MINING_DASHBOARD: boolean; + LIGHTNING: boolean; } const defaultEnv: Env = { @@ -60,7 +61,8 @@ const defaultEnv: Env = { 'MEMPOOL_WEBSITE_URL': 'https://mempool.space', 'LIQUID_WEBSITE_URL': 'https://liquid.network', 'BISQ_WEBSITE_URL': 'https://bisq.markets', - 'MINING_DASHBOARD': true + 'MINING_DASHBOARD': true, + 'LIGHTNING': false, }; @Injectable({ diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 1a799086b..a587c6934 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -40,7 +40,6 @@ import { BlockchainBlocksComponent } from '../components/blockchain-blocks/block import { AmountComponent } from '../components/amount/amount.component'; import { RouterModule } from '@angular/router'; import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe'; - import { StartComponent } from '../components/start/start.component'; import { TransactionComponent } from '../components/transaction/transaction.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; @@ -74,6 +73,7 @@ import { DataCyDirective } from '../data-cy.directive'; import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component'; import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component'; import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; +import { ChangeComponent } from '../components/change/change.component'; @NgModule({ declarations: [ @@ -104,7 +104,6 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen MempoolBlocksComponent, BlockchainBlocksComponent, AmountComponent, - AboutComponent, MasterPageComponent, BisqMasterPageComponent, @@ -142,6 +141,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen LoadingIndicatorComponent, IndexingProgressComponent, SvgImagesComponent, + ChangeComponent, ], imports: [ CommonModule, @@ -163,6 +163,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen NoSanitizePipe, ShortenStringPipe, CapAddressPipe, + AmountShortenerPipe, ], exports: [ RouterModule, @@ -203,7 +204,6 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen MempoolBlocksComponent, BlockchainBlocksComponent, AmountComponent, - StartComponent, TransactionComponent, BlockComponent, @@ -237,6 +237,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen LoadingIndicatorComponent, IndexingProgressComponent, SvgImagesComponent, + ChangeComponent, ] }) export class SharedModule { diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index e45742042..4ad9ffdd1 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -2,7 +2,8 @@ "MEMPOOL": { "NETWORK": "mainnet", "BACKEND": "lnd", - "HTTP_PORT": 8999, + "HTTP_PORT": 8899, + "API_URL_PREFIX": "/api/v1/", "STDOUT_LOG_MIN_PRIORITY": "debug" }, "SYSLOG": { diff --git a/lightning-backend/src/api/lightning-api-abstract-factory.ts b/lightning-backend/src/api/lightning/lightning-api-abstract-factory.ts similarity index 100% rename from lightning-backend/src/api/lightning-api-abstract-factory.ts rename to lightning-backend/src/api/lightning/lightning-api-abstract-factory.ts diff --git a/lightning-backend/src/api/lightning-api-factory.ts b/lightning-backend/src/api/lightning/lightning-api-factory.ts similarity index 89% rename from lightning-backend/src/api/lightning-api-factory.ts rename to lightning-backend/src/api/lightning/lightning-api-factory.ts index 4dfb3ae03..e2a730650 100644 --- a/lightning-backend/src/api/lightning-api-factory.ts +++ b/lightning-backend/src/api/lightning/lightning-api-factory.ts @@ -1,4 +1,4 @@ -import config from '../config'; +import config from '../../config'; import { AbstractLightningApi } from './lightning-api-abstract-factory'; import LndApi from './lnd/lnd-api'; diff --git a/lightning-backend/src/api/lightning-api.interface.ts b/lightning-backend/src/api/lightning/lightning-api.interface.ts similarity index 100% rename from lightning-backend/src/api/lightning-api.interface.ts rename to lightning-backend/src/api/lightning/lightning-api.interface.ts diff --git a/lightning-backend/src/api/lnd/lnd-api.ts b/lightning-backend/src/api/lightning/lnd/lnd-api.ts similarity index 94% rename from lightning-backend/src/api/lnd/lnd-api.ts rename to lightning-backend/src/api/lightning/lnd/lnd-api.ts index edddc31c6..3388a04b3 100644 --- a/lightning-backend/src/api/lnd/lnd-api.ts +++ b/lightning-backend/src/api/lightning/lnd/lnd-api.ts @@ -2,8 +2,8 @@ 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'; +import config from '../../../config'; +import logger from '../../../logger'; class LndApi implements AbstractLightningApi { private lnd: any; diff --git a/lightning-backend/src/api/nodes/nodes.api.ts b/lightning-backend/src/api/nodes/nodes.api.ts new file mode 100644 index 000000000..335ae61d2 --- /dev/null +++ b/lightning-backend/src/api/nodes/nodes.api.ts @@ -0,0 +1,42 @@ +import logger from '../../logger'; +import DB from '../../database'; + +class NodesApi { + public async $getTopCapacityNodes(): Promise { + try { + const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getTopChannelsNodes(): Promise { + try { + const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getLatestStatistics(): Promise { + try { + const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`); + const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 24`); + return { + latest: rows[0], + previous: rows2[0], + }; + } catch (e) { + logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } +} + +export default new NodesApi(); diff --git a/lightning-backend/src/api/nodes/nodes.routes.ts b/lightning-backend/src/api/nodes/nodes.routes.ts new file mode 100644 index 000000000..ac001f5dd --- /dev/null +++ b/lightning-backend/src/api/nodes/nodes.routes.ts @@ -0,0 +1,35 @@ +import config from '../../config'; +import { Express, Request, Response } from 'express'; +import nodesApi from './nodes.api'; +class NodesRoutes { + constructor(app: Express) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) + ; + } + + private async $getGeneralStats(req: Request, res: Response) { + try { + const statistics = await nodesApi.$getLatestStatistics(); + res.json(statistics); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + private async $getTopNodes(req: Request, res: Response) { + try { + const topCapacityNodes = await nodesApi.$getTopCapacityNodes(); + const topChannelsNodes = await nodesApi.$getTopChannelsNodes(); + res.json({ + topByCapacity: topCapacityNodes, + topByChannels: topChannelsNodes, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default NodesRoutes; diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index 4e9e36246..48c237174 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -5,6 +5,7 @@ interface IConfig { NETWORK: 'mainnet' | 'testnet' | 'signet'; BACKEND: 'lnd' | 'cln' | 'ldk'; HTTP_PORT: number; + API_URL_PREFIX: string; STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; }; SYSLOG: { @@ -33,6 +34,7 @@ const defaults: IConfig = { 'NETWORK': 'mainnet', 'BACKEND': 'lnd', 'HTTP_PORT': 8999, + 'API_URL_PREFIX': '/api/v1/', 'STDOUT_LOG_MIN_PRIORITY': 'debug', }, 'SYSLOG': { diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 4690fa0e0..cac997ef7 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -76,6 +76,7 @@ class DatabaseMigration { await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); + await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('nodes_stats')); } catch (e) { throw e; } @@ -204,28 +205,45 @@ class DatabaseMigration { private getCreateChannelsQuery(): string { return `CREATE TABLE IF NOT EXISTS channels ( id varchar(15) NOT NULL, - capacity double unsigned NOT NULL, + capacity bigint(20) unsigned NOT NULL, transaction_id varchar(64) NOT NULL, transaction_vout int(11) NOT NULL, - updated_at datetime NOT NULL, + updated_at datetime DEFAULT NULL, node1_public_key varchar(66) NOT NULL, - node1_base_fee_mtokens double unsigned NULL, - node1_cltv_delta int(11) NULL, - node1_fee_rate int(11) NULL, - node1_is_disabled boolean NULL, - node1_max_htlc_mtokens double unsigned NULL, - node1_min_htlc_mtokens double unsigned NULL, - node1_updated_at datetime NULL, + node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, + node1_cltv_delta int(11) DEFAULT NULL, + node1_fee_rate bigint(11) DEFAULT NULL, + node1_is_disabled tinyint(1) DEFAULT NULL, + node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node1_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node1_updated_at datetime DEFAULT NULL, node2_public_key varchar(66) NOT NULL, - node2_base_fee_mtokens double unsigned NULL, - node2_cltv_delta int(11) NULL, - node2_fee_rate int(11) NULL, - node2_is_disabled boolean NULL, - node2_max_htlc_mtokens double unsigned NULL, - node2_min_htlc_mtokens double unsigned NULL, - node2_updated_at datetime NULL, - CONSTRAINT PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, + node2_cltv_delta int(11) DEFAULT NULL, + node2_fee_rate bigint(11) DEFAULT NULL, + node2_is_disabled tinyint(1) DEFAULT NULL, + node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node2_updated_at datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY node1_public_key (node1_public_key), + KEY node2_public_key (node2_public_key), + KEY node1_public_key_2 (node1_public_key,node2_public_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateNodesStatsQuery(): string { + return `CREATE TABLE nodes_stats ( + id int(11) unsigned NOT NULL AUTO_INCREMENT, + public_key varchar(66) NOT NULL DEFAULT '', + added date NOT NULL, + capacity_left bigint(11) unsigned DEFAULT NULL, + capacity_right bigint(11) unsigned DEFAULT NULL, + channels_left int(11) unsigned DEFAULT NULL, + channels_right int(11) unsigned DEFAULT NULL, + PRIMARY KEY (id), + KEY public_key (public_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } } diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index a9e487ef5..d5e859f6e 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -1,13 +1,20 @@ import config from './config'; +import * as express from 'express'; +import * as http from 'http'; import logger from './logger'; import DB from './database'; +import { Express, Request, Response, NextFunction } from 'express'; import databaseMigration from './database-migration'; import statsUpdater from './tasks/stats-updater.service'; import nodeSyncService from './tasks/node-sync.service'; +import NodesRoutes from './api/nodes/nodes.routes'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); class LightningServer { + private server: http.Server | undefined; + private app: Express = express(); + constructor() { this.init(); } @@ -18,6 +25,27 @@ class LightningServer { statsUpdater.startService(); nodeSyncService.startService(); + + this.startServer(); + } + + startServer() { + this.app + .use((req: Request, res: Response, next: NextFunction) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + next(); + }) + .use(express.urlencoded({ extended: true })) + .use(express.text()) + ; + + this.server = http.createServer(this.app); + + this.server.listen(config.MEMPOOL.HTTP_PORT, () => { + logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); + }); + + const nodeRoutes = new NodesRoutes(this.app); } } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 8b5116be9..90e353028 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -1,8 +1,8 @@ import DB from '../database'; import logger from '../logger'; -import lightningApi from '../api/lightning-api-factory'; -import { ILightningApi } from '../api/lightning-api.interface'; +import lightningApi from '../api/lightning/lightning-api-factory'; +import { ILightningApi } from '../api/lightning/lightning-api.interface'; class NodeSyncService { constructor() {} diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index 2946977bc..c1ab1b250 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -1,7 +1,7 @@ import DB from '../database'; import logger from '../logger'; -import lightningApi from '../api/lightning-api-factory'; +import lightningApi from '../api/lightning/lightning-api-factory'; class LightningStatsUpdater { constructor() {} @@ -19,12 +19,25 @@ class LightningStatsUpdater { this.$logLightningStats(); }, 1000 * 60 * 60); }, difference); + + // this.$logNodeStatsDaily(); + } + + private async $logNodeStatsDaily() { + const query = `SELECT nodes.public_key, COUNT(DISTINCT c1.id) AS channels_count_left, COUNT(DISTINCT c2.id) AS channels_count_right, SUM(DISTINCT c1.capacity) AS channels_capacity_left, SUM(DISTINCT c2.capacity) AS channels_capacity_right FROM nodes LEFT JOIN channels AS c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN channels AS c2 ON c2.node2_public_key = nodes.public_key GROUP BY nodes.public_key`; + const [nodes]: any = await DB.query(query); + + for (const node of nodes) { + await DB.query( + `INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`, + [node.public_key, node.channels_capacity_left, node.channels_capacity_right, node.channels_count_left, node.channels_count_right]); + } } private async $logLightningStats() { - const networkInfo = await lightningApi.$getNetworkInfo(); - try { + const networkInfo = await lightningApi.$getNetworkInfo(); + const query = `INSERT INTO statistics( added, channel_count, From f5325b3a6d2880910296e0c180d41253a2bfa33f Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 29 Apr 2022 03:57:27 +0400 Subject: [PATCH 05/51] Node and channel API --- .../app/lightning/lightning-api.service.ts | 8 +++++ .../src/app/lightning/lightning.module.ts | 4 +++ .../app/lightning/lightning.routing.module.ts | 25 ++++++++++++++++ .../node-statistics.component.html | 12 ++++++++ .../app/lightning/node/node.component.html | 1 + .../app/lightning/node/node.component.scss | 0 .../src/app/lightning/node/node.component.ts | 29 +++++++++++++++++++ .../nodes-list/nodes-list.component.html | 2 +- .../src/api/nodes/channels.api.ts | 17 +++++++++++ .../src/api/nodes/channels.routes.ts | 23 +++++++++++++++ lightning-backend/src/api/nodes/nodes.api.ts | 19 +++++++++--- .../src/api/nodes/nodes.routes.ts | 15 ++++++++++ lightning-backend/src/database-migration.ts | 1 + lightning-backend/src/index.ts | 2 ++ .../src/tasks/stats-updater.service.ts | 29 ++++++++++++++----- 15 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 frontend/src/app/lightning/lightning.routing.module.ts create mode 100644 frontend/src/app/lightning/node/node.component.html create mode 100644 frontend/src/app/lightning/node/node.component.scss create mode 100644 frontend/src/app/lightning/node/node.component.ts create mode 100644 lightning-backend/src/api/nodes/channels.api.ts create mode 100644 lightning-backend/src/api/nodes/channels.routes.ts diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 3f36b55cd..aa8d1794f 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -12,6 +12,14 @@ export class LightningApiService { private httpClient: HttpClient, ) { } + getNode$(publicKey: string): Observable { + return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey); + } + + getChannelsByNodeId$(publicKey: string): Observable { + return this.httpClient.get(API_BASE_URL + '/channels/' + publicKey); + } + getLatestStatistics$(): Observable { return this.httpClient.get(API_BASE_URL + '/statistics/latest'); } diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index d98c0c910..4a4731f6d 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -6,16 +6,20 @@ import { LightningApiService } from './lightning-api.service'; import { NodesListComponent } from './nodes-list/nodes-list.component'; import { RouterModule } from '@angular/router'; import { NodeStatisticsComponent } from './node-statistics/node-statistics.component'; +import { NodeComponent } from './node/node.component'; +import { LightningRoutingModule } from './lightning.routing.module'; @NgModule({ declarations: [ LightningDashboardComponent, NodesListComponent, NodeStatisticsComponent, + NodeComponent, ], imports: [ CommonModule, SharedModule, RouterModule, + LightningRoutingModule, ], providers: [ LightningApiService, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts new file mode 100644 index 000000000..456436c8d --- /dev/null +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; +import { NodeComponent } from './node/node.component'; + +const routes: Routes = [ + { + path: '', + component: LightningDashboardComponent, + }, + { + path: 'node/:public_key', + component: NodeComponent, + }, + { + path: '**', + redirectTo: '' + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class LightningRoutingModule { } diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index 4a3842673..bac937093 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -1,5 +1,15 @@
+
+
Capacity
+
+ + + + +
+
Nodes
+
diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html new file mode 100644 index 000000000..458c9362e --- /dev/null +++ b/frontend/src/app/lightning/node/node.component.html @@ -0,0 +1 @@ +

node works!

diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts new file mode 100644 index 000000000..bfbc3d134 --- /dev/null +++ b/frontend/src/app/lightning/node/node.component.ts @@ -0,0 +1,29 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-node', + templateUrl: './node.component.html', + styleUrls: ['./node.component.scss'] +}) +export class NodeComponent implements OnInit { + node$: Observable; + + constructor( + private lightningApiService: LightningApiService, + private activatedRoute: ActivatedRoute, + ) { } + + ngOnInit(): void { + this.node$ = this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + return this.lightningApiService.getNode$(params.get('id')); + }) + ); + } + +} diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.html b/frontend/src/app/lightning/nodes-list/nodes-list.component.html index 441c2e0f1..64deb7b60 100644 --- a/frontend/src/app/lightning/nodes-list/nodes-list.component.html +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.html @@ -9,7 +9,7 @@ - {{ node.alias }} + {{ node.alias }} diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/nodes/channels.api.ts new file mode 100644 index 000000000..6b4905bd7 --- /dev/null +++ b/lightning-backend/src/api/nodes/channels.api.ts @@ -0,0 +1,17 @@ +import logger from '../../logger'; +import DB from '../../database'; + +class ChannelsApi { + public async $getChannelsForNode(public_key: string): Promise { + try { + const query = `SELECT * FROM channels WHERE node1_public_key = ? OR node2_public_key = ?`; + const [rows]: any = await DB.query(query, [public_key, public_key]); + return rows; + } catch (e) { + logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } +} + +export default new ChannelsApi(); diff --git a/lightning-backend/src/api/nodes/channels.routes.ts b/lightning-backend/src/api/nodes/channels.routes.ts new file mode 100644 index 000000000..70bf0bdad --- /dev/null +++ b/lightning-backend/src/api/nodes/channels.routes.ts @@ -0,0 +1,23 @@ +import config from '../../config'; +import { Express, Request, Response } from 'express'; +import channelsApi from './channels.api'; + +class ChannelsRoutes { + constructor(app: Express) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:public_key', this.$getChannels) + ; + } + + private async $getChannels(req: Request, res: Response) { + try { + const channels = await channelsApi.$getChannelsForNode(req.params.public_key); + res.json(channels); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + +} + +export default ChannelsRoutes; diff --git a/lightning-backend/src/api/nodes/nodes.api.ts b/lightning-backend/src/api/nodes/nodes.api.ts index 335ae61d2..c64ec1bf3 100644 --- a/lightning-backend/src/api/nodes/nodes.api.ts +++ b/lightning-backend/src/api/nodes/nodes.api.ts @@ -2,9 +2,20 @@ import logger from '../../logger'; import DB from '../../database'; class NodesApi { + public async $getNode(public_key: string): Promise { + try { + const query = `SELECT * FROM nodes WHERE public_key = ?`; + const [rows]: any = await DB.query(query, [public_key]); + return rows[0]; + } catch (e) { + logger.err('$getNode error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getTopCapacityNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`; + const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { @@ -15,7 +26,7 @@ class NodesApi { public async $getTopChannelsNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`; + const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { @@ -27,13 +38,13 @@ class NodesApi { public async $getLatestStatistics(): Promise { try { const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`); - const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 24`); + const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 71`); return { latest: rows[0], previous: rows2[0], }; } catch (e) { - logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); + logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e)); throw e; } } diff --git a/lightning-backend/src/api/nodes/nodes.routes.ts b/lightning-backend/src/api/nodes/nodes.routes.ts index ac001f5dd..ad254959c 100644 --- a/lightning-backend/src/api/nodes/nodes.routes.ts +++ b/lightning-backend/src/api/nodes/nodes.routes.ts @@ -1,14 +1,29 @@ import config from '../../config'; import { Express, Request, Response } from 'express'; import nodesApi from './nodes.api'; +import channelsApi from './channels.api'; class NodesRoutes { constructor(app: Express) { app .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) ; } + private async $getNode(req: Request, res: Response) { + try { + const node = await nodesApi.$getNode(req.params.public_key); + if (!node) { + res.status(404).send('Node not found'); + return; + } + res.json(node); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getGeneralStats(req: Request, res: Response) { try { const statistics = await nodesApi.$getLatestStatistics(); diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index cac997ef7..769783460 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -125,6 +125,7 @@ class DatabaseMigration { // Set initial values await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`); + await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`); } catch (e) { throw e; } diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index d5e859f6e..49275f782 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -8,6 +8,7 @@ import databaseMigration from './database-migration'; import statsUpdater from './tasks/stats-updater.service'; import nodeSyncService from './tasks/node-sync.service'; import NodesRoutes from './api/nodes/nodes.routes'; +import ChannelsRoutes from './api/nodes/channels.routes'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); @@ -46,6 +47,7 @@ class LightningServer { }); const nodeRoutes = new NodesRoutes(this.app); + const channelsRoutes = new ChannelsRoutes(this.app); } } diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index c1ab1b250..c96a3d6b4 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -15,22 +15,35 @@ class LightningStatsUpdater { setTimeout(() => { this.$logLightningStats(); + this.$logNodeStatsDaily(); setInterval(() => { this.$logLightningStats(); + this.$logNodeStatsDaily(); }, 1000 * 60 * 60); }, difference); - - // this.$logNodeStatsDaily(); } private async $logNodeStatsDaily() { - const query = `SELECT nodes.public_key, COUNT(DISTINCT c1.id) AS channels_count_left, COUNT(DISTINCT c2.id) AS channels_count_right, SUM(DISTINCT c1.capacity) AS channels_capacity_left, SUM(DISTINCT c2.capacity) AS channels_capacity_right FROM nodes LEFT JOIN channels AS c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN channels AS c2 ON c2.node2_public_key = nodes.public_key GROUP BY nodes.public_key`; - const [nodes]: any = await DB.query(query); + 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] === currentDate) { + return; + } - for (const node of nodes) { - await DB.query( - `INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`, - [node.public_key, node.channels_capacity_left, node.channels_capacity_right, node.channels_count_left, node.channels_count_right]); + 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 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 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`; + const [nodes]: any = await DB.query(query); + + for (const node of nodes) { + await DB.query( + `INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`, + [node.public_key, node.channels_capacity_left, node.channels_capacity_right, + node.channels_count_left, node.channels_count_right]); + } + await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]); + } catch (e) { + logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } } From 795bb6a7a6427b72fa501e786b65d14856b6badd Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 1 May 2022 03:01:27 +0400 Subject: [PATCH 06/51] Channel component --- .../lightning/channel/channel.component.html | 144 ++++++++++++++++++ .../lightning/channel/channel.component.scss | 3 + .../lightning/channel/channel.component.ts | 30 ++++ .../channels-list.component.html | 66 ++++++++ .../channels-list.component.scss | 0 .../channels-list/channels-list.component.ts | 23 +++ .../app/lightning/lightning-api.service.ts | 12 +- .../src/app/lightning/lightning.module.ts | 4 + .../app/lightning/lightning.routing.module.ts | 5 + .../node-statistics.component.html | 2 +- .../app/lightning/node/node.component.html | 67 +++++++- .../app/lightning/node/node.component.scss | 38 +++++ .../src/app/lightning/node/node.component.ts | 8 +- .../components/sats/sats.component.html | 5 + .../components/sats/sats.component.scss | 0 .../shared/components/sats/sats.component.ts | 32 ++++ frontend/src/app/shared/shared.module.ts | 3 + .../src/api/nodes/channels.api.ts | 13 +- .../src/api/nodes/channels.routes.ts | 30 +++- .../src/api/nodes/nodes.routes.ts | 15 +- lightning-backend/src/index.ts | 31 +--- lightning-backend/src/server.ts | 38 +++++ .../src/tasks/stats-updater.service.ts | 26 ++-- 23 files changed, 536 insertions(+), 59 deletions(-) create mode 100644 frontend/src/app/lightning/channel/channel.component.html create mode 100644 frontend/src/app/lightning/channel/channel.component.scss create mode 100644 frontend/src/app/lightning/channel/channel.component.ts create mode 100644 frontend/src/app/lightning/channels-list/channels-list.component.html create mode 100644 frontend/src/app/lightning/channels-list/channels-list.component.scss create mode 100644 frontend/src/app/lightning/channels-list/channels-list.component.ts create mode 100644 frontend/src/app/shared/components/sats/sats.component.html create mode 100644 frontend/src/app/shared/components/sats/sats.component.scss create mode 100644 frontend/src/app/shared/components/sats/sats.component.ts create mode 100644 lightning-backend/src/server.ts diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html new file mode 100644 index 000000000..c50680147 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -0,0 +1,144 @@ +
+
+

Channel {{ channel.id }}

+
+ Open +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + +
Last update{{ channel.updated_at | date:'yyyy-MM-dd HH:mm' }}
Transaction ID + + {{ channel.transaction_id | shortenString : 10 }} + + +
+
+
+
+ + + + + + + +
Capacity
+
+
+ +
+ +
+

Peers

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Node + {{ channel.alias_left }} +
+ + {{ channel.node1_public_key | shortenString : 18 }} + + +
Fee rate + {{ channel.node1_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Node + {{ channel.alias_right }} +
+ + {{ channel.node2_public_key | shortenString : 18 }} + + +
Fee rate + {{ channel.node2_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+ +
+ + +
+ +
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss new file mode 100644 index 000000000..ccf88f131 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -0,0 +1,3 @@ +.badges { + font-size: 18px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts new file mode 100644 index 000000000..b64e08353 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-channel', + templateUrl: './channel.component.html', + styleUrls: ['./channel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChannelComponent implements OnInit { + channel$: Observable; + + constructor( + private lightningApiService: LightningApiService, + private activatedRoute: ActivatedRoute, + ) { } + + ngOnInit(): void { + this.channel$ = this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + return this.lightningApiService.getChannel$(params.get('short_id')); + }) + ); + } + +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html new file mode 100644 index 000000000..77073ca64 --- /dev/null +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -0,0 +1,66 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Node AliasFee RateChannel IDCapacityTransaction ID
+ {{ channel.alias_left }} + + {{ channel.node1_fee_rate / 10000 | number }}% + + {{ channel.alias_right }} + + {{ channel.node2_fee_rate / 10000 | number }}% + + {{ channel.id }} + + + + + {{ channel.transaction_id | shortenString : 10 }} + + +
+ + + + + + + + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.scss b/frontend/src/app/lightning/channels-list/channels-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts new file mode 100644 index 000000000..f6b0c448b --- /dev/null +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-channels-list', + templateUrl: './channels-list.component.html', + styleUrls: ['./channels-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChannelsListComponent implements OnChanges { + @Input() publicKey: string; + channels$: Observable; + + constructor( + private lightningApiService: LightningApiService, + ) { } + + ngOnChanges(): void { + this.channels$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey); + } + +} diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index aa8d1794f..326ac063f 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; const API_BASE_URL = '/lightning/api/v1'; @@ -16,8 +16,16 @@ export class LightningApiService { return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey); } + getChannel$(shortId: string): Observable { + return this.httpClient.get(API_BASE_URL + '/channels/' + shortId); + } + getChannelsByNodeId$(publicKey: string): Observable { - return this.httpClient.get(API_BASE_URL + '/channels/' + publicKey); + let params = new HttpParams() + .set('public_key', publicKey) + ; + + return this.httpClient.get(API_BASE_URL + '/channels', { params }); } getLatestStatistics$(): Observable { diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 4a4731f6d..3cdf2a281 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -8,12 +8,16 @@ import { RouterModule } from '@angular/router'; import { NodeStatisticsComponent } from './node-statistics/node-statistics.component'; import { NodeComponent } from './node/node.component'; import { LightningRoutingModule } from './lightning.routing.module'; +import { ChannelsListComponent } from './channels-list/channels-list.component'; +import { ChannelComponent } from './channel/channel.component'; @NgModule({ declarations: [ LightningDashboardComponent, NodesListComponent, NodeStatisticsComponent, NodeComponent, + ChannelsListComponent, + ChannelComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 456436c8d..c04e34f23 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; import { NodeComponent } from './node/node.component'; +import { ChannelComponent } from './channel/channel.component'; const routes: Routes = [ { @@ -12,6 +13,10 @@ const routes: Routes = [ path: 'node/:public_key', component: NodeComponent, }, + { + path: 'channel/:short_id', + component: ChannelComponent, + }, { path: '**', redirectTo: '' diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index bac937093..0bfbce647 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -4,7 +4,7 @@
Capacity
- + diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 458c9362e..c85f63c78 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -1 +1,66 @@ -

node works!

+
+ + +
+ +
+ +
+
+ + + + + + + + + + + + + + + +
First Seen{{ node.first_seen | date:'yyyy-MM-dd HH:mm' }}
Updated At{{ node.updated_at | date:'yyyy-MM-dd HH:mm' }}
Color
{{ node.color }}
+
+
+
+
+ +
+
+
+ +
+ +
+

Channels

+ + + + + +
+ +
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index e69de29bb..5ff5de482 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -0,0 +1,38 @@ +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; +} + +.qrcode-col { + margin: 20px auto 10px; + text-align: center; + @media (min-width: 992px){ + margin: 0px auto 0px; + } +} + +.tx-link { + display: flex; + flex-grow: 1; + @media (min-width: 650px) { + align-self: end; + margin-left: 15px; + margin-top: 0px; + margin-bottom: -3px; + } + @media (min-width: 768px) { + margin-bottom: 4px; + top: 1px; + position: relative; + } + @media (max-width: 768px) { + order: 3; + } +} + +.title-container { + display: flex; + flex-direction: row; +} diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index bfbc3d134..b62198d0d 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @@ -7,10 +7,12 @@ import { LightningApiService } from '../lightning-api.service'; @Component({ selector: 'app-node', templateUrl: './node.component.html', - styleUrls: ['./node.component.scss'] + styleUrls: ['./node.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodeComponent implements OnInit { node$: Observable; + publicKey$: Observable; constructor( private lightningApiService: LightningApiService, @@ -21,7 +23,7 @@ export class NodeComponent implements OnInit { this.node$ = this.activatedRoute.paramMap .pipe( switchMap((params: ParamMap) => { - return this.lightningApiService.getNode$(params.get('id')); + return this.lightningApiService.getNode$(params.get('public_key')); }) ); } diff --git a/frontend/src/app/shared/components/sats/sats.component.html b/frontend/src/app/shared/components/sats/sats.component.html new file mode 100644 index 000000000..8358812a9 --- /dev/null +++ b/frontend/src/app/shared/components/sats/sats.component.html @@ -0,0 +1,5 @@ +‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} +L- +tL- +t- +s-sats diff --git a/frontend/src/app/shared/components/sats/sats.component.scss b/frontend/src/app/shared/components/sats/sats.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/sats/sats.component.ts b/frontend/src/app/shared/components/sats/sats.component.ts new file mode 100644 index 000000000..f341b3027 --- /dev/null +++ b/frontend/src/app/shared/components/sats/sats.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../../services/state.service'; + +@Component({ + selector: 'app-sats', + templateUrl: './sats.component.html', + styleUrls: ['./sats.component.scss'] +}) +export class SatsComponent implements OnInit { + @Input() satoshis: number; + @Input() digitsInfo = 0; + @Input() addPlus = false; + + network = ''; + stateSubscription: Subscription; + + constructor( + private stateService: StateService, + ) { } + + ngOnInit() { + this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); + } + + ngOnDestroy() { + if (this.stateSubscription) { + this.stateSubscription.unsubscribe(); + } + } + +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index a587c6934..7e8914000 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -74,6 +74,7 @@ import { LoadingIndicatorComponent } from '../components/loading-indicator/loadi import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component'; import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; import { ChangeComponent } from '../components/change/change.component'; +import { SatsComponent } from './components/sats/sats.component'; @NgModule({ declarations: [ @@ -142,6 +143,7 @@ import { ChangeComponent } from '../components/change/change.component'; IndexingProgressComponent, SvgImagesComponent, ChangeComponent, + SatsComponent, ], imports: [ CommonModule, @@ -238,6 +240,7 @@ import { ChangeComponent } from '../components/change/change.component'; IndexingProgressComponent, SvgImagesComponent, ChangeComponent, + SatsComponent ] }) export class SharedModule { diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/nodes/channels.api.ts index 6b4905bd7..9f02981c3 100644 --- a/lightning-backend/src/api/nodes/channels.api.ts +++ b/lightning-backend/src/api/nodes/channels.api.ts @@ -2,9 +2,20 @@ import logger from '../../logger'; import DB from '../../database'; class ChannelsApi { + public async $getChannel(shortId: string): Promise { + try { + 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.id = ?`; + const [rows]: any = await DB.query(query, [shortId]); + return rows[0]; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannelsForNode(public_key: string): Promise { try { - const query = `SELECT * FROM channels WHERE node1_public_key = ? OR node2_public_key = ?`; + 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 node1_public_key = ? OR node2_public_key = ?`; const [rows]: any = await DB.query(query, [public_key, public_key]); return rows; } catch (e) { diff --git a/lightning-backend/src/api/nodes/channels.routes.ts b/lightning-backend/src/api/nodes/channels.routes.ts index 70bf0bdad..4cb3f8b1d 100644 --- a/lightning-backend/src/api/nodes/channels.routes.ts +++ b/lightning-backend/src/api/nodes/channels.routes.ts @@ -3,16 +3,36 @@ import { Express, Request, Response } from 'express'; import channelsApi from './channels.api'; class ChannelsRoutes { - constructor(app: Express) { + constructor() { } + + public initRoutes(app: Express) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:public_key', this.$getChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) ; } + private async $getChannel(req: Request, res: Response) { + try { + const channel = await channelsApi.$getChannel(req.params.short_id); + if (!channel) { + res.status(404).send('Channel not found'); + return; + } + res.json(channel); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getChannels(req: Request, res: Response) { try { - const channels = await channelsApi.$getChannelsForNode(req.params.public_key); - res.json(channels); + if (typeof req.query.public_key !== 'string') { + res.status(501).send('Missing parameter: public_key'); + return; + } + const channels = await channelsApi.$getChannelsForNode(req.query.public_key); + res.json(channels); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } @@ -20,4 +40,4 @@ class ChannelsRoutes { } -export default ChannelsRoutes; +export default new ChannelsRoutes(); diff --git a/lightning-backend/src/api/nodes/nodes.routes.ts b/lightning-backend/src/api/nodes/nodes.routes.ts index ad254959c..73bef9f26 100644 --- a/lightning-backend/src/api/nodes/nodes.routes.ts +++ b/lightning-backend/src/api/nodes/nodes.routes.ts @@ -1,14 +1,15 @@ import config from '../../config'; import { Express, Request, Response } from 'express'; import nodesApi from './nodes.api'; -import channelsApi from './channels.api'; class NodesRoutes { - constructor(app: Express) { + constructor() { } + + public initRoutes(app: Express) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) + ; } private async $getNode(req: Request, res: Response) { @@ -47,4 +48,4 @@ class NodesRoutes { } } -export default NodesRoutes; +export default new NodesRoutes(); diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index 49275f782..614ac8499 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -1,21 +1,14 @@ import config from './config'; -import * as express from 'express'; -import * as http from 'http'; import logger from './logger'; import DB from './database'; -import { Express, Request, Response, NextFunction } from 'express'; import databaseMigration from './database-migration'; import statsUpdater from './tasks/stats-updater.service'; import nodeSyncService from './tasks/node-sync.service'; -import NodesRoutes from './api/nodes/nodes.routes'; -import ChannelsRoutes from './api/nodes/channels.routes'; +import server from './server'; logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); class LightningServer { - private server: http.Server | undefined; - private app: Express = express(); - constructor() { this.init(); } @@ -27,27 +20,7 @@ class LightningServer { statsUpdater.startService(); nodeSyncService.startService(); - this.startServer(); - } - - startServer() { - this.app - .use((req: Request, res: Response, next: NextFunction) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - next(); - }) - .use(express.urlencoded({ extended: true })) - .use(express.text()) - ; - - this.server = http.createServer(this.app); - - this.server.listen(config.MEMPOOL.HTTP_PORT, () => { - logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); - }); - - const nodeRoutes = new NodesRoutes(this.app); - const channelsRoutes = new ChannelsRoutes(this.app); + server.startServer(); } } diff --git a/lightning-backend/src/server.ts b/lightning-backend/src/server.ts new file mode 100644 index 000000000..26608b0c4 --- /dev/null +++ b/lightning-backend/src/server.ts @@ -0,0 +1,38 @@ +import { Express, Request, Response, NextFunction } from 'express'; +import * as express from 'express'; +import * as http from 'http'; +import logger from './logger'; +import config from './config'; +import nodesRoutes from './api/nodes/nodes.routes'; +import channelsRoutes from './api/nodes/channels.routes'; + +class Server { + private server: http.Server | undefined; + private app: Express = express(); + + public startServer() { + this.app + .use((req: Request, res: Response, next: NextFunction) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + next(); + }) + .use(express.urlencoded({ extended: true })) + .use(express.text()) + ; + + this.server = http.createServer(this.app); + + this.server.listen(config.MEMPOOL.HTTP_PORT, () => { + logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); + }); + + this.initRoutes(); + } + + private initRoutes() { + nodesRoutes.initRoutes(this.app); + channelsRoutes.initRoutes(this.app); + } +} + +export default new Server(); diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index c96a3d6b4..0c61922e9 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -15,12 +15,13 @@ class LightningStatsUpdater { setTimeout(() => { this.$logLightningStats(); - this.$logNodeStatsDaily(); setInterval(() => { this.$logLightningStats(); this.$logNodeStatsDaily(); }, 1000 * 60 * 60); }, difference); + + this.$logNodeStatsDaily(); } private async $logNodeStatsDaily() { @@ -28,7 +29,7 @@ class LightningStatsUpdater { try { const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`); // Only store once per day - if (state[0] === currentDate) { + if (state[0].string === currentDate) { return; } @@ -42,6 +43,7 @@ class LightningStatsUpdater { node.channels_count_left, node.channels_count_right]); } await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]); + logger.debug('Daily node stats has updated.'); } catch (e) { logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } @@ -49,22 +51,26 @@ class LightningStatsUpdater { private async $logLightningStats() { try { - const networkInfo = await lightningApi.$getNetworkInfo(); + const networkGraph = await lightningApi.$getNetworkGraph(); + let total_capacity = 0; + for (const channel of networkGraph.channels) { + if (channel.capacity) { + total_capacity += channel.capacity; + } + } const query = `INSERT INTO statistics( added, channel_count, node_count, - total_capacity, - average_channel_size + total_capacity ) - VALUES (NOW(), ?, ?, ?, ?)`; + VALUES (NOW(), ?, ?, ?)`; await DB.query(query, [ - networkInfo.channel_count, - networkInfo.node_count, - networkInfo.total_capacity, - networkInfo.average_channel_size + networkGraph.channels.length, + networkGraph.nodes.length, + total_capacity, ]); } catch (e) { logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e)); From 65c731e1ad0d11d61cbabd99d7aef86d45903775 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 1 May 2022 15:35:28 +0400 Subject: [PATCH 07/51] Channel status --- .../lightning/channel/channel.component.html | 173 +++++++++--------- .../lightning/channel/channel.component.scss | 2 +- .../channels-list.component.html | 11 ++ lightning-backend/mempool-config.sample.json | 6 + .../src/api/bitcoin/bitcoin-client.ts | 12 ++ .../src/api/bitcoin/rpc-api/commands.ts | 92 ++++++++++ .../src/api/bitcoin/rpc-api/index.ts | 61 ++++++ .../src/api/bitcoin/rpc-api/jsonrpc.ts | 162 ++++++++++++++++ .../src/api/nodes/channels.api.ts | 22 +++ lightning-backend/src/config.ts | 14 ++ .../src/tasks/node-sync.service.ts | 38 +++- 11 files changed, 502 insertions(+), 91 deletions(-) create mode 100644 lightning-backend/src/api/bitcoin/bitcoin-client.ts create mode 100644 lightning-backend/src/api/bitcoin/rpc-api/commands.ts create mode 100644 lightning-backend/src/api/bitcoin/rpc-api/index.ts create mode 100644 lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index c50680147..c775135cb 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -2,7 +2,9 @@

Channel {{ channel.id }}

- Open + Inactive + Active + Closed
@@ -46,98 +48,95 @@

-

Peers

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Node - {{ channel.alias_left }} -
- - {{ channel.node1_public_key | shortenString : 18 }} - - -
Fee rate - {{ channel.node1_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
+
+
+ -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Node - {{ channel.alias_right }} -
- - {{ channel.node2_public_key | shortenString : 18 }} - - -
Fee rate - {{ channel.node2_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Fee rate + {{ channel.node1_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Fee rate + {{ channel.node2_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
- +
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss index ccf88f131..a6878a23c 100644 --- a/frontend/src/app/lightning/channel/channel.component.scss +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -1,3 +1,3 @@ .badges { - font-size: 18px; + font-size: 20px; } \ No newline at end of file diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 77073ca64..2ca201ca6 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -3,6 +3,7 @@ + @@ -14,6 +15,11 @@ + @@ -22,6 +28,11 @@ + diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index 4ad9ffdd1..eac34ddf4 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -17,6 +17,12 @@ "TSL_CERT_PATH": "", "MACAROON_PATH": "" }, + "CORE_RPC": { + "HOST": "127.0.0.1", + "PORT": 8332, + "USERNAME": "mempool", + "PASSWORD": "mempool" + }, "DATABASE": { "HOST": "127.0.0.1", "PORT": 3306, diff --git a/lightning-backend/src/api/bitcoin/bitcoin-client.ts b/lightning-backend/src/api/bitcoin/bitcoin-client.ts new file mode 100644 index 000000000..43e76a041 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-client.ts @@ -0,0 +1,12 @@ +import config from '../../config'; +const bitcoin = require('./rpc-api/index'); + +const nodeRpcCredentials: any = { + host: config.CORE_RPC.HOST, + port: config.CORE_RPC.PORT, + user: config.CORE_RPC.USERNAME, + pass: config.CORE_RPC.PASSWORD, + timeout: 60000, +}; + +export default new bitcoin.Client(nodeRpcCredentials); diff --git a/lightning-backend/src/api/bitcoin/rpc-api/commands.ts b/lightning-backend/src/api/bitcoin/rpc-api/commands.ts new file mode 100644 index 000000000..ea9bd7bf0 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/commands.ts @@ -0,0 +1,92 @@ +module.exports = { + addMultiSigAddress: 'addmultisigaddress', + addNode: 'addnode', // bitcoind v0.8.0+ + backupWallet: 'backupwallet', + createMultiSig: 'createmultisig', + createRawTransaction: 'createrawtransaction', // bitcoind v0.7.0+ + decodeRawTransaction: 'decoderawtransaction', // bitcoind v0.7.0+ + decodeScript: 'decodescript', + dumpPrivKey: 'dumpprivkey', + dumpWallet: 'dumpwallet', // bitcoind v0.9.0+ + encryptWallet: 'encryptwallet', + estimateFee: 'estimatefee', // bitcoind v0.10.0x + estimatePriority: 'estimatepriority', // bitcoind v0.10.0+ + generate: 'generate', // bitcoind v0.11.0+ + getAccount: 'getaccount', + getAccountAddress: 'getaccountaddress', + getAddedNodeInfo: 'getaddednodeinfo', // bitcoind v0.8.0+ + getAddressesByAccount: 'getaddressesbyaccount', + getBalance: 'getbalance', + getBestBlockHash: 'getbestblockhash', // bitcoind v0.9.0+ + getBlock: 'getblock', + getBlockStats: 'getblockstats', + getBlockFilter: 'getblockfilter', + getBlockchainInfo: 'getblockchaininfo', // bitcoind v0.9.2+ + getBlockCount: 'getblockcount', + getBlockHash: 'getblockhash', + getBlockHeader: 'getblockheader', + getBlockTemplate: 'getblocktemplate', // bitcoind v0.7.0+ + getChainTips: 'getchaintips', // bitcoind v0.10.0+ + getChainTxStats: 'getchaintxstats', + getConnectionCount: 'getconnectioncount', + getDifficulty: 'getdifficulty', + getGenerate: 'getgenerate', + getInfo: 'getinfo', + getMempoolAncestors: 'getmempoolancestors', + getMempoolDescendants: 'getmempooldescendants', + getMempoolEntry: 'getmempoolentry', + getMempoolInfo: 'getmempoolinfo', // bitcoind v0.10+ + getMiningInfo: 'getmininginfo', + getNetTotals: 'getnettotals', + getNetworkInfo: 'getnetworkinfo', // bitcoind v0.9.2+ + getNetworkHashPs: 'getnetworkhashps', // bitcoind v0.9.0+ + getNewAddress: 'getnewaddress', + getPeerInfo: 'getpeerinfo', // bitcoind v0.7.0+ + getRawChangeAddress: 'getrawchangeaddress', // bitcoin v0.9+ + getRawMemPool: 'getrawmempool', // bitcoind v0.7.0+ + getRawTransaction: 'getrawtransaction', // bitcoind v0.7.0+ + getReceivedByAccount: 'getreceivedbyaccount', + getReceivedByAddress: 'getreceivedbyaddress', + getTransaction: 'gettransaction', + getTxOut: 'gettxout', // bitcoind v0.7.0+ + getTxOutProof: 'gettxoutproof', // bitcoind v0.11.0+ + getTxOutSetInfo: 'gettxoutsetinfo', // bitcoind v0.7.0+ + getUnconfirmedBalance: 'getunconfirmedbalance', // bitcoind v0.9.0+ + getWalletInfo: 'getwalletinfo', // bitcoind v0.9.2+ + help: 'help', + importAddress: 'importaddress', // bitcoind v0.10.0+ + importPrivKey: 'importprivkey', + importWallet: 'importwallet', // bitcoind v0.9.0+ + keypoolRefill: 'keypoolrefill', + keyPoolRefill: 'keypoolrefill', + listAccounts: 'listaccounts', + listAddressGroupings: 'listaddressgroupings', // bitcoind v0.7.0+ + listLockUnspent: 'listlockunspent', // bitcoind v0.8.0+ + listReceivedByAccount: 'listreceivedbyaccount', + listReceivedByAddress: 'listreceivedbyaddress', + listSinceBlock: 'listsinceblock', + listTransactions: 'listtransactions', + listUnspent: 'listunspent', // bitcoind v0.7.0+ + lockUnspent: 'lockunspent', // bitcoind v0.8.0+ + move: 'move', + ping: 'ping', // bitcoind v0.9.0+ + prioritiseTransaction: 'prioritisetransaction', // bitcoind v0.10.0+ + sendFrom: 'sendfrom', + sendMany: 'sendmany', + sendRawTransaction: 'sendrawtransaction', // bitcoind v0.7.0+ + sendToAddress: 'sendtoaddress', + setAccount: 'setaccount', + setGenerate: 'setgenerate', + setTxFee: 'settxfee', + signMessage: 'signmessage', + signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ + stop: 'stop', + submitBlock: 'submitblock', // bitcoind v0.7.0+ + validateAddress: 'validateaddress', + verifyChain: 'verifychain', // bitcoind v0.9.0+ + verifyMessage: 'verifymessage', + verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+ + walletLock: 'walletlock', + walletPassphrase: 'walletpassphrase', + walletPassphraseChange: 'walletpassphrasechange' +} diff --git a/lightning-backend/src/api/bitcoin/rpc-api/index.ts b/lightning-backend/src/api/bitcoin/rpc-api/index.ts new file mode 100644 index 000000000..131e1a048 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/index.ts @@ -0,0 +1,61 @@ +var commands = require('./commands') +var rpc = require('./jsonrpc') + +// ===----------------------------------------------------------------------===// +// JsonRPC +// ===----------------------------------------------------------------------===// +function Client (opts) { + // @ts-ignore + this.rpc = new rpc.JsonRPC(opts) +} + +// ===----------------------------------------------------------------------===// +// cmd +// ===----------------------------------------------------------------------===// +Client.prototype.cmd = function () { + var args = [].slice.call(arguments) + var cmd = args.shift() + + callRpc(cmd, args, this.rpc) +} + +// ===----------------------------------------------------------------------===// +// callRpc +// ===----------------------------------------------------------------------===// +function callRpc (cmd, args, rpc) { + var fn = args[args.length - 1] + + // If the last argument is a callback, pop it from the args list + if (typeof fn === 'function') { + args.pop() + } else { + fn = function () {} + } + + return rpc.call(cmd, args, function () { + var args = [].slice.call(arguments) + // @ts-ignore + args.unshift(null) + // @ts-ignore + fn.apply(this, args) + }, function (err) { + fn(err) + }) +} + +// ===----------------------------------------------------------------------===// +// Initialize wrappers +// ===----------------------------------------------------------------------===// +(function () { + for (var protoFn in commands) { + (function (protoFn) { + Client.prototype[protoFn] = function () { + var args = [].slice.call(arguments) + return callRpc(commands[protoFn], args, this.rpc) + } + })(protoFn) + } +})() + +// Export! +module.exports.Client = Client; diff --git a/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts b/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts new file mode 100644 index 000000000..4f7a38baa --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts @@ -0,0 +1,162 @@ +var http = require('http') +var https = require('https') + +var JsonRPC = function (opts) { + // @ts-ignore + this.opts = opts || {} + // @ts-ignore + this.http = this.opts.ssl ? https : http +} + +JsonRPC.prototype.call = function (method, params) { + return new Promise((resolve, reject) => { + var time = Date.now() + var requestJSON + + if (Array.isArray(method)) { + // multiple rpc batch call + requestJSON = [] + method.forEach(function (batchCall, i) { + requestJSON.push({ + id: time + '-' + i, + method: batchCall.method, + params: batchCall.params + }) + }) + } else { + // single rpc call + requestJSON = { + id: time, + method: method, + params: params + } + } + + // First we encode the request into JSON + requestJSON = JSON.stringify(requestJSON) + + // prepare request options + var requestOptions = { + host: this.opts.host || 'localhost', + port: this.opts.port || 8332, + method: 'POST', + path: '/', + headers: { + 'Host': this.opts.host || 'localhost', + 'Content-Length': requestJSON.length + }, + agent: false, + rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false + } + + if (this.opts.ssl && this.opts.sslCa) { + // @ts-ignore + requestOptions.ca = this.opts.sslCa + } + + // use HTTP auth if user and password set + if (this.opts.user && this.opts.pass) { + // @ts-ignore + requestOptions.auth = this.opts.user + ':' + this.opts.pass + } + + // Now we'll make a request to the server + var cbCalled = false + var request = this.http.request(requestOptions) + + // start request timeout timer + var reqTimeout = setTimeout(function () { + if (cbCalled) return + cbCalled = true + request.abort() + var err = new Error('ETIMEDOUT') + // @ts-ignore + err.code = 'ETIMEDOUT' + reject(err) + }, this.opts.timeout || 30000) + + // set additional timeout on socket in case of remote freeze after sending headers + request.setTimeout(this.opts.timeout || 30000, function () { + if (cbCalled) return + cbCalled = true + request.abort() + var err = new Error('ESOCKETTIMEDOUT') + // @ts-ignore + err.code = 'ESOCKETTIMEDOUT' + reject(err) + }) + + request.on('error', function (err) { + if (cbCalled) return + cbCalled = true + clearTimeout(reqTimeout) + reject(err) + }) + + request.on('response', function (response) { + clearTimeout(reqTimeout) + + // We need to buffer the response chunks in a nonblocking way. + var buffer = '' + response.on('data', function (chunk) { + buffer = buffer + chunk + }) + // When all the responses are finished, we decode the JSON and + // depending on whether it's got a result or an error, we call + // emitSuccess or emitError on the promise. + response.on('end', function () { + var err + + if (cbCalled) return + cbCalled = true + + try { + var decoded = JSON.parse(buffer) + } catch (e) { + if (response.statusCode !== 200) { + err = new Error('Invalid params, response status code: ' + response.statusCode) + err.code = -32602 + reject(err) + } else { + err = new Error('Problem parsing JSON response from server') + err.code = -32603 + reject(err) + } + return + } + + if (!Array.isArray(decoded)) { + decoded = [decoded] + } + + // iterate over each response, normally there will be just one + // unless a batch rpc call response is being processed + decoded.forEach(function (decodedResponse, i) { + if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) { + if (reject) { + err = new Error(decodedResponse.error.message || '') + if (decodedResponse.error.code) { + err.code = decodedResponse.error.code + } + reject(err) + } + } else if (decodedResponse.hasOwnProperty('result')) { + // @ts-ignore + resolve(decodedResponse.result, response.headers) + } else { + if (reject) { + err = new Error(decodedResponse.error.message || '') + if (decodedResponse.error.code) { + err.code = decodedResponse.error.code + } + reject(err) + } + } + }) + }) + }) + request.end(requestJSON); + }); +} + +module.exports.JsonRPC = JsonRPC diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/nodes/channels.api.ts index 9f02981c3..157cbc97a 100644 --- a/lightning-backend/src/api/nodes/channels.api.ts +++ b/lightning-backend/src/api/nodes/channels.api.ts @@ -2,6 +2,28 @@ import logger from '../../logger'; import DB from '../../database'; class ChannelsApi { + public async $getAllChannels(): Promise { + try { + const query = `SELECT * FROM channels`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getChannelsByStatus(status: number): Promise { + try { + const query = `SELECT * FROM channels WHERE status = ?`; + const [rows]: any = await DB.query(query, [status]); + return rows; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannel(shortId: string): Promise { try { 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.id = ?`; diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index 48c237174..9b71dd977 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -19,6 +19,12 @@ interface IConfig { TSL_CERT_PATH: string; MACAROON_PATH: string; }; + CORE_RPC: { + HOST: string; + PORT: number; + USERNAME: string; + PASSWORD: string; + }; DATABASE: { HOST: string, SOCKET: string, @@ -48,6 +54,12 @@ const defaults: IConfig = { 'TSL_CERT_PATH': '', 'MACAROON_PATH': '', }, + 'CORE_RPC': { + 'HOST': '127.0.0.1', + 'PORT': 8332, + 'USERNAME': 'mempool', + 'PASSWORD': 'mempool' + }, 'DATABASE': { 'HOST': '127.0.0.1', 'SOCKET': '', @@ -62,6 +74,7 @@ class Config implements IConfig { MEMPOOL: IConfig['MEMPOOL']; SYSLOG: IConfig['SYSLOG']; LN_NODE_AUTH: IConfig['LN_NODE_AUTH']; + CORE_RPC: IConfig['CORE_RPC']; DATABASE: IConfig['DATABASE']; constructor() { @@ -69,6 +82,7 @@ class Config implements IConfig { this.MEMPOOL = configs.MEMPOOL; this.SYSLOG = configs.SYSLOG; this.LN_NODE_AUTH = configs.LN_NODE_AUTH; + this.CORE_RPC = configs.CORE_RPC; this.DATABASE = configs.DATABASE; } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 90e353028..cd257a483 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -3,6 +3,8 @@ import DB from '../database'; import logger from '../logger'; import lightningApi from '../api/lightning/lightning-api-factory'; import { ILightningApi } from '../api/lightning/lightning-api.interface'; +import channelsApi from '../api/nodes/channels.api'; +import bitcoinClient from '../api/bitcoin/bitcoin-client'; class NodeSyncService { constructor() {} @@ -25,15 +27,35 @@ class NodeSyncService { await this.$saveNode(node); } + await this.$setChannelsInactive(); + for (const channel of networkGraph.channels) { await this.$saveChannel(channel); } + + await this.$scanForClosedChannels(); + + } catch (e) { + logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $scanForClosedChannels() { + try { + const channels = await channelsApi.$getChannelsByStatus(0); + for (const channel of channels) { + const outspends = await bitcoinClient.getTxOut(channel.transaction_id, channel.transaction_vout); + if (outspends === null) { + logger.debug('Marking channel: ' + channel.id + ' as closed.'); + await DB.query(`UPDATE channels SET status = 2 WHERE id = ?`, [channel.id]); + } + } } catch (e) { logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); } } - private async $saveChannel(channel: ILightningApi.Channel) { + private async $saveChannel(channel: ILightningApi.Channel): Promise { try { const d = new Date(Date.parse(channel.updated_at)); const query = `INSERT INTO channels @@ -43,6 +65,7 @@ class NodeSyncService { transaction_id, transaction_vout, updated_at, + status, node1_public_key, node1_base_fee_mtokens, node1_cltv_delta, @@ -60,10 +83,11 @@ class NodeSyncService { node2_min_htlc_mtokens, node2_updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE capacity = ?, updated_at = ?, + status = 1, node1_public_key = ?, node1_base_fee_mtokens = ?, node1_cltv_delta = ?, @@ -128,7 +152,15 @@ class NodeSyncService { } } - private async $saveNode(node: ILightningApi.Node) { + private async $setChannelsInactive(): Promise { + try { + await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`); + } catch (e) { + logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveNode(node: ILightningApi.Node): Promise { try { const updatedAt = this.utcDateToMysql(node.updated_at); const query = `INSERT INTO nodes( From 7e1c2f4f405f0a63b3508caf72a1c60228eba7a4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 3 May 2022 20:18:07 +0400 Subject: [PATCH 08/51] Find inactive channels with dead nodes. --- .../lightning/channel/channel.component.html | 4 +- .../channels-list.component.html | 37 ++++++++++++------- .../node-statistics.component.html | 6 +-- .../api/{nodes => explorer}/channels.api.ts | 0 .../{nodes => explorer}/channels.routes.ts | 0 .../src/api/{nodes => explorer}/nodes.api.ts | 2 +- .../api/{nodes => explorer}/nodes.routes.ts | 0 lightning-backend/src/server.ts | 4 +- .../src/tasks/node-sync.service.ts | 27 +++++++++++++- 9 files changed, 56 insertions(+), 24 deletions(-) rename lightning-backend/src/api/{nodes => explorer}/channels.api.ts (100%) rename lightning-backend/src/api/{nodes => explorer}/channels.routes.ts (100%) rename lightning-backend/src/api/{nodes => explorer}/nodes.api.ts (98%) rename lightning-backend/src/api/{nodes => explorer}/nodes.routes.ts (100%) diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index c775135cb..29a55df57 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -52,7 +52,7 @@
-

{{ channel.alias_left }}

+

{{ channel.alias_left || '?' }}

{{ channel.node1_public_key | shortenString : 18 }} @@ -96,7 +96,7 @@
-

{{ channel.alias_right }}

+

{{ channel.alias_right || '?' }}

{{ channel.node2_public_key | shortenString : 18 }} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 2ca201ca6..88b3c4ab1 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -3,17 +3,23 @@
Node AliasStatus Fee Rate Channel ID Capacity {{ channel.alias_left }} + Inactive + Active + Closed + {{ channel.node1_fee_rate / 10000 | number }}% {{ channel.alias_right }} + Inactive + Active + Closed + {{ channel.node2_fee_rate / 10000 | number }}%
+ - - + + - + - - + + diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index 0bfbce647..6991e2b66 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -3,7 +3,7 @@
Capacity
+ ngbTooltip="Percentage change past week" placement="bottom"> @@ -13,7 +13,7 @@
Nodes
+ ngbTooltip="Percentage change past week" placement="bottom">
{{ statistics.latest.node_count | number }}
@@ -25,7 +25,7 @@
Channels
+ ngbTooltip="Percentage change past week" placement="bottom">
{{ statistics.latest.channel_count | number }}
diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts similarity index 100% rename from lightning-backend/src/api/nodes/channels.api.ts rename to lightning-backend/src/api/explorer/channels.api.ts diff --git a/lightning-backend/src/api/nodes/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts similarity index 100% rename from lightning-backend/src/api/nodes/channels.routes.ts rename to lightning-backend/src/api/explorer/channels.routes.ts diff --git a/lightning-backend/src/api/nodes/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts similarity index 98% rename from lightning-backend/src/api/nodes/nodes.api.ts rename to lightning-backend/src/api/explorer/nodes.api.ts index c64ec1bf3..5ed30c0e4 100644 --- a/lightning-backend/src/api/nodes/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -38,7 +38,7 @@ class NodesApi { public async $getLatestStatistics(): Promise { try { const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`); - const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 71`); + const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 72`); return { latest: rows[0], previous: rows2[0], diff --git a/lightning-backend/src/api/nodes/nodes.routes.ts b/lightning-backend/src/api/explorer/nodes.routes.ts similarity index 100% rename from lightning-backend/src/api/nodes/nodes.routes.ts rename to lightning-backend/src/api/explorer/nodes.routes.ts diff --git a/lightning-backend/src/server.ts b/lightning-backend/src/server.ts index 26608b0c4..954be4f0a 100644 --- a/lightning-backend/src/server.ts +++ b/lightning-backend/src/server.ts @@ -3,8 +3,8 @@ import * as express from 'express'; import * as http from 'http'; import logger from './logger'; import config from './config'; -import nodesRoutes from './api/nodes/nodes.routes'; -import channelsRoutes from './api/nodes/channels.routes'; +import nodesRoutes from './api/explorer/nodes.routes'; +import channelsRoutes from './api/explorer/channels.routes'; class Server { private server: http.Server | undefined; diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index cd257a483..57d301642 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -3,7 +3,7 @@ import DB from '../database'; import logger from '../logger'; import lightningApi from '../api/lightning/lightning-api-factory'; import { ILightningApi } from '../api/lightning/lightning-api.interface'; -import channelsApi from '../api/nodes/channels.api'; +import channelsApi from '../api/explorer/channels.api'; import bitcoinClient from '../api/bitcoin/bitcoin-client'; class NodeSyncService { @@ -33,6 +33,7 @@ class NodeSyncService { await this.$saveChannel(channel); } + await this.$findInactiveNodesAndChannels(); await this.$scanForClosedChannels(); } catch (e) { @@ -40,7 +41,21 @@ class NodeSyncService { } } - private async $scanForClosedChannels() { + // Looking for channels whos nodes are inactive + private async $findInactiveNodesAndChannels(): Promise { + try { + // @ts-ignore + const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`); + + for (const channel of channels) { + await this.$updateChannelStatus(channel.id, 0); + } + } catch (e) { + logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $scanForClosedChannels(): Promise { try { const channels = await channelsApi.$getChannelsByStatus(0); for (const channel of channels) { @@ -152,6 +167,14 @@ class NodeSyncService { } } + private async $updateChannelStatus(channelShortId: string, status: number): Promise { + try { + await DB.query(`UPDATE channels SET status = ? WHERE id = ?`, [status, channelShortId]); + } catch (e) { + logger.err('$updateChannelStatus() error: ' + (e instanceof Error ? e.message : e)); + } + } + private async $setChannelsInactive(): Promise { try { await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`); From b0b73e6c70eb86b7fd80f5bdcc0080fcf2bf456f Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 3 May 2022 20:55:34 +0400 Subject: [PATCH 09/51] Adding channel id in addition to short id --- backend/package-lock.json | 27 +++++++++++++++++++ backend/package.json | 1 + .../lightning/channel/channel.component.html | 2 +- .../channels-list.component.html | 2 +- .../src/tasks/node-sync.service.ts | 8 ++++-- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 494aa7cf2..e140675a5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "^16.11.41", "axios": "~0.27.2", "bitcoinjs-lib": "6.0.1", + "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", "mysql2": "2.3.3", @@ -798,6 +799,11 @@ "node": ">=8.0.0" } }, + "node_modules/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, "node_modules/body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -821,6 +827,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bolt07": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", + "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", + "dependencies": { + "bn.js": "5.2.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3397,6 +3411,11 @@ "wif": "^2.0.1" } }, + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, "body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -3416,6 +3435,14 @@ "unpipe": "1.0.0" } }, + "bolt07": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", + "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", + "requires": { + "bn.js": "5.2.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/backend/package.json b/backend/package.json index 67fc12f85..d3fb90e21 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,6 +34,7 @@ "@types/node": "^16.11.41", "axios": "~0.27.2", "bitcoinjs-lib": "6.0.1", + "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", "mysql2": "2.3.3", diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 29a55df57..76db429e3 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -1,6 +1,6 @@
-

Channel {{ channel.id }}

+

Channel {{ channel.short_id }}

Inactive Active diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 88b3c4ab1..066d37a70 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -53,7 +53,7 @@
diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 57d301642..952090f31 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -1,4 +1,4 @@ - +import { chanNumber } from 'bolt07'; import DB from '../database'; import logger from '../logger'; import lightningApi from '../api/lightning/lightning-api-factory'; @@ -71,11 +71,14 @@ class NodeSyncService { } private async $saveChannel(channel: ILightningApi.Channel): Promise { + const fromChannel = chanNumber({ channel: channel.id }).number; + try { const d = new Date(Date.parse(channel.updated_at)); const query = `INSERT INTO channels ( id, + short_id, capacity, transaction_id, transaction_vout, @@ -98,7 +101,7 @@ class NodeSyncService { node2_min_htlc_mtokens, node2_updated_at ) - VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE capacity = ?, updated_at = ?, @@ -122,6 +125,7 @@ class NodeSyncService { ;`; await DB.query(query, [ + fromChannel, channel.id, channel.capacity, channel.transaction_id, From 07821769cd05310403e50b287fbad7ebcd6f01e8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 5 May 2022 23:19:24 +0400 Subject: [PATCH 10/51] Node stats updates --- .../time-since/time-since.component.ts | 9 ++- .../app/lightning/lightning-api.service.ts | 4 ++ .../app/lightning/node/node.component.html | 12 ++-- .../src/app/lightning/node/node.component.ts | 8 +++ .../nodes-list/nodes-list.component.html | 4 +- .../src/api/explorer/channels.api.ts | 11 ++++ .../src/api/explorer/nodes.api.ts | 19 ++++-- .../src/api/explorer/nodes.routes.ts | 10 ++++ lightning-backend/src/database-migration.ts | 6 +- .../src/tasks/node-sync.service.ts | 58 ++++++++++++++++++- .../src/tasks/stats-updater.service.ts | 6 +- 11 files changed, 127 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/time-since/time-since.component.ts b/frontend/src/app/components/time-since/time-since.component.ts index 0fbf745de..1162116ec 100644 --- a/frontend/src/app/components/time-since/time-since.component.ts +++ b/frontend/src/app/components/time-since/time-since.component.ts @@ -13,6 +13,7 @@ export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy { intervals = {}; @Input() time: number; + @Input() dateString: number; @Input() fastRender = false; constructor( @@ -52,7 +53,13 @@ export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy { } calculate() { - const seconds = Math.floor((+new Date() - +new Date(this.time * 1000)) / 1000); + let date: Date; + if (this.dateString) { + date = new Date(this.dateString) + } else { + date = new Date(this.time * 1000); + } + const seconds = Math.floor((+new Date() - +date) / 1000); if (seconds < 60) { return $localize`:@@date-base.just-now:Just now`; } diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 326ac063f..a49923546 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -32,6 +32,10 @@ export class LightningApiService { return this.httpClient.get(API_BASE_URL + '/statistics/latest'); } + listNodeStats$(publicKey: string): Observable { + return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey + '/statistics'); + } + listTopNodes$(): Observable { return this.httpClient.get(API_BASE_URL + '/nodes/top'); } diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index c85f63c78..c4d618dcf 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -18,12 +18,16 @@
Node AliasNode ID Status Fee RateChannel ID CapacityTransaction IDChannel ID
- {{ channel.alias_left }} + {{ channel.alias_left || '?' }} + + + {{ channel.node1_public_key | shortenString : 10 }} + + Inactive @@ -25,8 +31,14 @@ - {{ channel.alias_right }} + + {{ channel.alias_right || '?' }} + + + {{ channel.node2_public_key | shortenString : 10 }} + + Inactive @@ -37,23 +49,20 @@ {{ channel.node2_fee_rate / 10000 | number }}% - {{ channel.id }} - - - {{ channel.transaction_id | shortenString : 10 }} - - - + {{ channel.id }} +
+ + - {{ channel.id }} + {{ channel.short_id }}
- - + + - - + + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index b62198d0d..6949e1826 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -12,6 +12,7 @@ import { LightningApiService } from '../lightning-api.service'; }) export class NodeComponent implements OnInit { node$: Observable; + statistics$: Observable; publicKey$: Observable; constructor( @@ -26,6 +27,13 @@ export class NodeComponent implements OnInit { return this.lightningApiService.getNode$(params.get('public_key')); }) ); + + this.statistics$ = this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + return this.lightningApiService.listNodeStats$(params.get('public_key')); + }) + ); } } diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.html b/frontend/src/app/lightning/nodes-list/nodes-list.component.html index 64deb7b60..65a7a558a 100644 --- a/frontend/src/app/lightning/nodes-list/nodes-list.component.html +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.html @@ -12,10 +12,10 @@ {{ node.alias }} diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index 157cbc97a..c13bc9319 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -24,6 +24,17 @@ class ChannelsApi { } } + public async $getChannelsWithoutCreatedDate(): Promise { + 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; + } + } + public async $getChannel(shortId: string): Promise { try { 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.id = ?`; diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index 5ed30c0e4..0b034d230 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -4,8 +4,8 @@ import DB from '../../database'; class NodesApi { public async $getNode(public_key: string): Promise { try { - const query = `SELECT * FROM nodes WHERE public_key = ?`; - const [rows]: any = await DB.query(query, [public_key]); + const query = `SELECT nodes.*, (SELECT COUNT(*) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS channel_count, (SELECT SUM(capacity) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS capacity FROM nodes WHERE public_key = ?`; + const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key]); return rows[0]; } catch (e) { logger.err('$getNode error: ' + (e instanceof Error ? e.message : e)); @@ -13,9 +13,20 @@ class NodesApi { } } + public async $getNodeStats(public_key: string): Promise { + try { + const query = `SELECT * FROM nodes_stats WHERE public_key = ? ORDER BY added DESC`; + 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; + } + } + public async $getTopCapacityNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.capacity_left + nodes_stats.capacity_right DESC LIMIT 10`; + const query = `SELECT nodes.*, nodes_stats.capacity, nodes_stats.channels FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.capacity DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { @@ -26,7 +37,7 @@ class NodesApi { public async $getTopChannelsNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity_left, nodes_stats.capacity_right, nodes_stats.channels_left, nodes_stats.channels_right FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.channels_left + nodes_stats.channels_right DESC LIMIT 10`; + const query = `SELECT nodes.*, nodes_stats.capacity, nodes_stats.channels FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.channels DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { diff --git a/lightning-backend/src/api/explorer/nodes.routes.ts b/lightning-backend/src/api/explorer/nodes.routes.ts index 73bef9f26..1b86abb69 100644 --- a/lightning-backend/src/api/explorer/nodes.routes.ts +++ b/lightning-backend/src/api/explorer/nodes.routes.ts @@ -8,6 +8,7 @@ class NodesRoutes { app .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) ; } @@ -25,6 +26,15 @@ class NodesRoutes { } } + private async $getHistoricalNodeStats(req: Request, res: Response) { + try { + const statistics = await nodesApi.$getNodeStats(req.params.public_key); + res.json(statistics); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getGeneralStats(req: Request, res: Response) { try { const statistics = await nodesApi.$getLatestStatistics(); diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 769783460..8c16da676 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -238,10 +238,8 @@ class DatabaseMigration { id int(11) unsigned NOT NULL AUTO_INCREMENT, public_key varchar(66) NOT NULL DEFAULT '', added date NOT NULL, - capacity_left bigint(11) unsigned DEFAULT NULL, - capacity_right bigint(11) unsigned DEFAULT NULL, - channels_left int(11) unsigned DEFAULT NULL, - channels_right int(11) unsigned DEFAULT NULL, + capacity bigint(11) unsigned DEFAULT NULL, + channels int(11) unsigned DEFAULT NULL, PRIMARY KEY (id), KEY public_key (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 952090f31..1479ce13a 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -26,21 +26,71 @@ class NodeSyncService { for (const node of networkGraph.nodes) { await this.$saveNode(node); } + logger.debug(`Nodes updated`); await this.$setChannelsInactive(); for (const channel of networkGraph.channels) { await this.$saveChannel(channel); } + logger.debug(`Channels updated`); await this.$findInactiveNodesAndChannels(); + logger.debug(`Inactive channels scan complete`); + await this.$scanForClosedChannels(); + logger.debug(`Closed channels scan complete`); + + await this.$lookUpCreationDateFromChain(); + logger.debug(`Channel creation dates scan complete`); + + await this.$updateNodeFirstSeen(); + logger.debug(`Node first seen dates scan complete`); } catch (e) { logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); } } + // This method look up the creation date of the earliest channel of the node + // and update the node to that date in order to get the earliest first seen date + private async $updateNodeFirstSeen() { + try { + const [nodes]: any[] = await DB.query(`SELECT nodes.public_key, UNIX_TIMESTAMP(nodes.first_seen) AS first_seen, (SELECT UNIX_TIMESTAMP(created) FROM channels WHERE channels.node1_public_key = nodes.public_key ORDER BY created ASC LIMIT 1) AS created1, (SELECT UNIX_TIMESTAMP(created) FROM channels WHERE channels.node2_public_key = nodes.public_key ORDER BY created ASC LIMIT 1) AS created2 FROM nodes`); + for (const node of nodes) { + let lowest = 0; + if (node.created1) { + if (node.created2 && node.created2 < node.created1) { + lowest = node.created2; + } else { + lowest = node.created1; + } + } else if (node.created2) { + lowest = node.created2; + } + if (lowest && lowest < node.first_seen) { + const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`; + const params = [lowest, node.public_key]; + await DB.query(query, params); + } + } + } catch (e) { + logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $lookUpCreationDateFromChain() { + try { + const channels = await channelsApi.$getChannelsWithoutCreatedDate(); + for (const channel of channels) { + const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1); + await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]); + } + } catch (e) { + logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e)); + } + } + // Looking for channels whos nodes are inactive private async $findInactiveNodesAndChannels(): Promise { try { @@ -190,23 +240,27 @@ class NodeSyncService { private async $saveNode(node: ILightningApi.Node): Promise { try { const updatedAt = this.utcDateToMysql(node.updated_at); + const sockets = node.sockets.join(', '); const query = `INSERT INTO nodes( public_key, first_seen, updated_at, alias, - color + color, + sockets ) - VALUES (?, NOW(), ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?;`; + VALUES (?, NOW(), ?, ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?, sockets = ?;`; await DB.query(query, [ node.public_key, updatedAt, node.alias, node.color, + sockets, updatedAt, node.alias, node.color, + sockets, ]); } catch (e) { logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e)); diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index 0c61922e9..c96273ff1 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -38,9 +38,9 @@ class LightningStatsUpdater { for (const node of nodes) { await DB.query( - `INSERT INTO nodes_stats(public_key, added, capacity_left, capacity_right, channels_left, channels_right) VALUES (?, NOW(), ?, ?, ?, ?)`, - [node.public_key, node.channels_capacity_left, node.channels_capacity_right, - node.channels_count_left, node.channels_count_right]); + `INSERT INTO nodes_stats(public_key, added, capacity, channels) VALUES (?, NOW(), ?, ?)`, + [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]); } await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]); logger.debug('Daily node stats has updated.'); From 774215a073a30c723a28b31741064f1f42def2ad Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 6 May 2022 00:20:14 +0400 Subject: [PATCH 11/51] Socket selector and copy --- .../app/lightning/node/node.component.html | 18 ++++++++++++ .../src/app/lightning/node/node.component.ts | 29 +++++++++++++++++-- .../src/tasks/node-sync.service.ts | 2 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index c4d618dcf..b573346d9 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -45,6 +45,24 @@ + +
+ +
+
+ +
+ +
+
+ + {{ node.socketsObject[selectedSocketIndex].label }} + + + +

Channels

diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 6949e1826..503cc1471 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -14,6 +14,7 @@ export class NodeComponent implements OnInit { node$: Observable; statistics$: Observable; publicKey$: Observable; + selectedSocketIndex = 0; constructor( private lightningApiService: LightningApiService, @@ -25,7 +26,27 @@ export class NodeComponent implements OnInit { .pipe( switchMap((params: ParamMap) => { return this.lightningApiService.getNode$(params.get('public_key')); - }) + }), + map((node) => { + const socketsObject = []; + for (const socket of node.sockets.split(',')) { + let label = ''; + if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { + label = 'IPv4'; + } else if (socket.indexOf('[') > -1) { + label = 'IPv6'; + } else if (socket.indexOf('onion') > -1) { + label = 'Tor'; + } + socketsObject.push({ + label: label, + socket: node.public_key + '@' + socket, + }); + } + console.log(socketsObject); + node.socketsObject = socketsObject; + return node; + }), ); this.statistics$ = this.activatedRoute.paramMap @@ -36,4 +57,8 @@ export class NodeComponent implements OnInit { ); } + changeSocket(index: number) { + this.selectedSocketIndex = index; + } + } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 1479ce13a..65ecec8c2 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -240,7 +240,7 @@ class NodeSyncService { private async $saveNode(node: ILightningApi.Node): Promise { try { const updatedAt = this.utcDateToMysql(node.updated_at); - const sockets = node.sockets.join(', '); + const sockets = node.sockets.join(','); const query = `INSERT INTO nodes( public_key, first_seen, From d23e5d0e87b5727cf8ae6d296add7c3ec4c7ddf8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 6 May 2022 00:52:25 +0400 Subject: [PATCH 12/51] Node qr code --- .../clipboard/clipboard.component.scss | 5 +++++ .../app/components/qrcode/qrcode.component.ts | 16 ++++++++++++++-- .../src/app/lightning/node/node.component.html | 18 +++++++++++------- .../src/app/lightning/node/node.component.scss | 15 +++++++++++++++ .../src/app/lightning/node/node.component.ts | 4 ++++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/clipboard/clipboard.component.scss b/frontend/src/app/components/clipboard/clipboard.component.scss index 9483fef52..be173d821 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.scss +++ b/frontend/src/app/components/clipboard/clipboard.component.scss @@ -1,3 +1,8 @@ .btn-link { padding: 0.25rem 0 0.1rem 0.5rem; } + +img { + position: relative; + left: -3px; +} \ No newline at end of file diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index 30c8a8362..07f2703fc 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -1,11 +1,12 @@ -import { Component, Input, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, AfterViewInit, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'; import * as QRCode from 'qrcode'; import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-qrcode', templateUrl: './qrcode.component.html', - styleUrls: ['./qrcode.component.scss'] + styleUrls: ['./qrcode.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class QrcodeComponent implements AfterViewInit { @Input() data: string; @@ -19,7 +20,18 @@ export class QrcodeComponent implements AfterViewInit { private stateService: StateService, ) { } + ngOnChanges() { + if (!this.canvas.nativeElement) { + return; + } + this.render(); + } + ngAfterViewInit() { + this.render(); + } + + render() { if (!this.stateService.isBrowser) { return; } diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index b573346d9..713cb2709 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -38,9 +38,7 @@
-
- -
+
@@ -48,9 +46,9 @@
-
-
- +
+
+
@@ -59,7 +57,13 @@ {{ node.socketsObject[selectedSocketIndex].label }} - +
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index 5ff5de482..c3831ded9 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -3,6 +3,21 @@ padding: 10px; padding-bottom: 5px; display: inline-block; + + + position: absolute; + bottom: 50px; + left: -175px; + z-index: 100; +} + +.dropdownLabel { + min-width: 50px; + display: inline-block; +} + +#inputGroupFileAddon04 { + position: relative; } .qrcode-col { diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 503cc1471..f3c0d6153 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -15,6 +15,7 @@ export class NodeComponent implements OnInit { statistics$: Observable; publicKey$: Observable; selectedSocketIndex = 0; + qrCodeVisible = false; constructor( private lightningApiService: LightningApiService, @@ -30,6 +31,9 @@ export class NodeComponent implements OnInit { map((node) => { const socketsObject = []; for (const socket of node.sockets.split(',')) { + if (socket === '') { + continue; + } let label = ''; if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { label = 'IPv4'; From 31d280f7298dc9366b0c2b7f09f9a693241934a0 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 6 May 2022 16:31:25 +0400 Subject: [PATCH 13/51] Adding Lightning wrapper component --- .../lightning/channel/channel.component.html | 8 +++-- .../lightning-wrapper.component.html | 1 + .../lightning-wrapper.component.scss | 0 .../lightning-wrapper.component.ts | 16 ++++++++++ .../src/app/lightning/lightning.module.ts | 2 ++ .../app/lightning/lightning.routing.module.ts | 29 +++++++++++++------ 6 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.html create mode 100644 frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.scss create mode 100644 frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 76db429e3..649773b41 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -16,9 +16,13 @@
First Seen{{ node.first_seen | date:'yyyy-MM-dd HH:mm' }}First seen + +
Updated At{{ node.updated_at | date:'yyyy-MM-dd HH:mm' }}Last update + +
Color - + - {{ node.channels_left + node.channels_right | number }} + {{ node.channels | number }}
+ + + + - + @@ -38,7 +42,7 @@ - +
Created
Last update{{ channel.updated_at | date:'yyyy-MM-dd HH:mm' }}
Transaction ID
Capacity 
diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.html b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.html new file mode 100644 index 000000000..0680b43f9 --- /dev/null +++ b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.scss b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts new file mode 100644 index 000000000..40bcc83bc --- /dev/null +++ b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { WebsocketService } from 'src/app/services/websocket.service'; + +@Component({ + selector: 'app-lightning-wrapper', + templateUrl: './lightning-wrapper.component.html', + styleUrls: ['./lightning-wrapper.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LightningWrapperComponent { + + constructor( + private websocketService: WebsocketService, + ) { } + +} diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 3cdf2a281..ffb1fd356 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -10,6 +10,7 @@ import { NodeComponent } from './node/node.component'; import { LightningRoutingModule } from './lightning.routing.module'; import { ChannelsListComponent } from './channels-list/channels-list.component'; import { ChannelComponent } from './channel/channel.component'; +import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -18,6 +19,7 @@ import { ChannelComponent } from './channel/channel.component'; NodeComponent, ChannelsListComponent, ChannelComponent, + LightningWrapperComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index c04e34f23..e56a527f9 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -1,21 +1,32 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LightningDashboardComponent } from './lightning-dashboard/lightning-dashboard.component'; +import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; import { NodeComponent } from './node/node.component'; import { ChannelComponent } from './channel/channel.component'; const routes: Routes = [ { path: '', - component: LightningDashboardComponent, - }, - { - path: 'node/:public_key', - component: NodeComponent, - }, - { - path: 'channel/:short_id', - component: ChannelComponent, + component: LightningWrapperComponent, + children: [ + { + path: '', + component: LightningDashboardComponent, + }, + { + path: 'node/:public_key', + component: NodeComponent, + }, + { + path: 'channel/:short_id', + component: ChannelComponent, + }, + { + path: '**', + redirectTo: '' + } + ] }, { path: '**', From 67eab93129f093cf139a4d8df06919c7a1bfb029 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 7 May 2022 11:32:15 +0400 Subject: [PATCH 14/51] Link channels from Transaction page. --- .../address-labels.component.html | 17 +++++++++---- .../address-labels.component.ts | 15 ++++++++---- .../transactions-list.component.html | 2 +- .../transactions-list.component.ts | 19 +++++++++++---- frontend/src/app/services/api.service.ts | 9 +++++++ .../src/api/explorer/channels.api.ts | 15 ++++++++++-- .../src/api/explorer/channels.routes.ts | 24 +++++++++++++++++++ 7 files changed, 86 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index ec59684a5..353e733ae 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,4 +1,13 @@ -{{ label }} + + {{ label }} + + + + {{ label }} + \ No newline at end of file diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index c5892fd8a..dbac4ab11 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Vin, Vout } from '../../interfaces/electrs.interface'; import { StateService } from 'src/app/services/state.service'; @@ -8,11 +8,12 @@ import { StateService } from 'src/app/services/state.service'; styleUrls: ['./address-labels.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AddressLabelsComponent implements OnInit { +export class AddressLabelsComponent implements OnChanges { network = ''; @Input() vin: Vin; @Input() vout: Vout; + @Input() channel: any; label?: string; @@ -22,14 +23,20 @@ export class AddressLabelsComponent implements OnInit { this.network = stateService.network; } - ngOnInit() { - if (this.vin) { + ngOnChanges() { + if (this.channel) { + this.handleChannel(); + } else if (this.vin) { this.handleVin(); } else if (this.vout) { this.handleVout(); } } + handleChannel() { + this.label = `Channel open: ${this.channel.alias_left} <> ${this.channel.alias_right}`; + } + handleVin() { if (this.vin.inner_witnessscript_asm) { if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) { diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 16e6b9b2f..9119355f6 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -172,7 +172,7 @@
- +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index d5ec36151..ab0a742cf 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { StateService } from '../../services/state.service'; -import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; +import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from 'src/environments/environment'; @@ -32,9 +32,11 @@ export class TransactionsListComponent implements OnInit, OnChanges { latestBlock$: Observable; outspendsSubscription: Subscription; refreshOutspends$: ReplaySubject = new ReplaySubject(); + refreshChannels$: ReplaySubject = new ReplaySubject(); showDetails$ = new BehaviorSubject(false); outspends: Outspend[][] = []; assetsMinimal: any; + channels: any[]; constructor( public stateService: StateService, @@ -73,7 +75,15 @@ export class TransactionsListComponent implements OnInit, OnChanges { }; } }), - ) + ), + this.refreshChannels$ + .pipe( + switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), + map((channels) => { + this.channels = channels; + }), + ) + , ).subscribe(() => this.ref.markForCheck()); } @@ -114,8 +124,9 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } }); - - this.refreshOutspends$.next(this.transactions.map((tx) => tx.txid)); + const txIds = this.transactions.map((tx) => tx.txid); + this.refreshOutspends$.next(txIds); + this.refreshChannels$.next(txIds); } onScroll() { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index a0b3d8ff7..ad552ffb1 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -231,4 +231,13 @@ export class ApiService { getRewardStats$(blockCount: number = 144): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); } + + getChannelByTxIds$(txIds: string[]): Observable { + let params = new HttpParams(); + txIds.forEach((txId: string) => { + params = params.append('txId[]', txId); + }); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params }); + } + } diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index c13bc9319..be2c953f5 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -8,7 +8,7 @@ class ChannelsApi { const [rows]: any = await DB.query(query); return rows; } catch (e) { - logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + logger.err('$getAllChannels error: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -19,7 +19,7 @@ class ChannelsApi { const [rows]: any = await DB.query(query, [status]); return rows; } catch (e) { - logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + logger.err('$getChannelsByStatus error: ' + (e instanceof Error ? e.message : e)); throw e; } } @@ -46,6 +46,17 @@ class ChannelsApi { } } + public async $getChannelByTransactionId(transactionId: string): Promise { + try { + 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 = ?`; + const [rows]: any = await DB.query(query, [transactionId]); + return rows[0]; + } catch (e) { + logger.err('$getChannelByTransactionId error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannelsForNode(public_key: string): Promise { try { 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 node1_public_key = ? OR node2_public_key = ?`; diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts index 4cb3f8b1d..07dabdd31 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/lightning-backend/src/api/explorer/channels.routes.ts @@ -7,6 +7,7 @@ class ChannelsRoutes { public initRoutes(app: Express) { app + .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) ; @@ -38,6 +39,29 @@ class ChannelsRoutes { } } + private async $getChannelsByTransactionIds(req: Request, res: Response) { + try { + if (!Array.isArray(req.query.txId)) { + res.status(500).send('Not an array'); + return; + } + const txIds: string[] = []; + for (const _txId in req.query.txId) { + if (typeof req.query.txId[_txId] === 'string') { + txIds.push(req.query.txId[_txId].toString()); + } + } + const channels: any[] = []; + for (const txId of txIds) { + const channel = await channelsApi.$getChannelByTransactionId(txId); + channels.push(channel); + } + res.json(channels); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + } export default new ChannelsRoutes(); From caadae3f983c18d525de7c8083c409f901546ab1 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 8 May 2022 16:16:54 +0400 Subject: [PATCH 15/51] Updated migration script with latest database model. --- .../node-statistics.component.html | 12 ++++---- .../src/api/explorer/nodes.api.ts | 6 ++-- lightning-backend/src/database-migration.ts | 28 +++++++++++-------- .../src/tasks/stats-updater.service.ts | 7 ++++- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.html b/frontend/src/app/lightning/node-statistics/node-statistics.component.html index 6991e2b66..f45daa7fd 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.html +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.html @@ -4,8 +4,8 @@
Capacity
- - + +
@@ -15,9 +15,9 @@
- {{ statistics.latest.node_count | number }} + {{ statistics.latest?.node_count || 0 | number }}
- +
@@ -27,9 +27,9 @@
- {{ statistics.latest.channel_count | number }} + {{ statistics.latest?.channel_count || 0 | number }}
- +
diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index 0b034d230..119336723 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -15,7 +15,7 @@ class NodesApi { public async $getNodeStats(public_key: string): Promise { try { - const query = `SELECT * FROM nodes_stats WHERE public_key = ? ORDER BY added DESC`; + const query = `SELECT * FROM node_stats WHERE public_key = ? ORDER BY added DESC`; const [rows]: any = await DB.query(query, [public_key]); return rows; } catch (e) { @@ -26,7 +26,7 @@ class NodesApi { public async $getTopCapacityNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity, nodes_stats.channels FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.capacity DESC LIMIT 10`; + const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.capacity DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { @@ -37,7 +37,7 @@ class NodesApi { public async $getTopChannelsNodes(): Promise { try { - const query = `SELECT nodes.*, nodes_stats.capacity, nodes_stats.channels FROM nodes LEFT JOIN nodes_stats ON nodes_stats.public_key = nodes.public_key ORDER BY nodes_stats.added DESC, nodes_stats.channels DESC LIMIT 10`; + const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.channels DESC LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 8c16da676..37b5301cf 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -76,7 +76,7 @@ class DatabaseMigration { await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); - await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('nodes_stats')); + await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); } catch (e) { throw e; } @@ -187,8 +187,7 @@ class DatabaseMigration { channel_count int(11) NOT NULL, node_count int(11) NOT NULL, total_capacity double unsigned NOT NULL, - average_channel_size double unsigned NOT NULL, - CONSTRAINT PRIMARY KEY (id) + PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } @@ -197,26 +196,30 @@ class DatabaseMigration { public_key varchar(66) NOT NULL, first_seen datetime NOT NULL, updated_at datetime NOT NULL, - alias varchar(200) COLLATE utf8mb4_general_ci NOT NULL, + alias varchar(200) CHARACTER SET utf8mb4 NOT NULL, color varchar(200) NOT NULL, - CONSTRAINT PRIMARY KEY (public_key) + sockets text DEFAULT NULL, + PRIMARY KEY (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } private getCreateChannelsQuery(): string { return `CREATE TABLE IF NOT EXISTS channels ( - id varchar(15) NOT NULL, + id bigint(11) unsigned NOT NULL, + short_id varchar(15) NOT NULL DEFAULT '', capacity bigint(20) unsigned NOT NULL, transaction_id varchar(64) NOT NULL, transaction_vout int(11) NOT NULL, updated_at datetime DEFAULT NULL, + created datetime DEFAULT NULL, + status int(11) NOT NULL DEFAULT 0, node1_public_key varchar(66) NOT NULL, node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, node1_cltv_delta int(11) DEFAULT NULL, node1_fee_rate bigint(11) DEFAULT NULL, node1_is_disabled tinyint(1) DEFAULT NULL, node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, - node1_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node1_min_htlc_mtokens bigint(20) DEFAULT NULL, node1_updated_at datetime DEFAULT NULL, node2_public_key varchar(66) NOT NULL, node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, @@ -229,18 +232,21 @@ class DatabaseMigration { PRIMARY KEY (id), KEY node1_public_key (node1_public_key), KEY node2_public_key (node2_public_key), - KEY node1_public_key_2 (node1_public_key,node2_public_key) + KEY status (status), + KEY short_id (short_id), + KEY transaction_id (transaction_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } private getCreateNodesStatsQuery(): string { - return `CREATE TABLE nodes_stats ( + return `CREATE TABLE IF NOT EXISTS node_stats ( id int(11) unsigned NOT NULL AUTO_INCREMENT, public_key varchar(66) NOT NULL DEFAULT '', added date NOT NULL, - capacity bigint(11) unsigned DEFAULT NULL, - channels int(11) unsigned DEFAULT NULL, + capacity bigint(20) unsigned NOT NULL DEFAULT 0, + channels int(11) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), + UNIQUE KEY added (added,public_key), KEY public_key (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index c96273ff1..c0a6392b1 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -36,9 +36,14 @@ class LightningStatsUpdater { 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 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 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`; const [nodes]: any = await DB.query(query); + // First run we won't have any nodes yet + if (nodes.length < 10) { + return; + } + for (const node of nodes) { await DB.query( - `INSERT INTO nodes_stats(public_key, added, capacity, channels) VALUES (?, NOW(), ?, ?)`, + `INSERT INTO node_stats(public_key, added, capacity, channels) VALUES (?, NOW(), ?, ?)`, [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]); } From 1ed4c93b944a3a347555ba26f6108b5ad655d6a4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 9 May 2022 18:21:42 +0400 Subject: [PATCH 16/51] Search API --- frontend/src/app/services/api.service.ts | 5 +++ .../src/api/explorer/channels.api.ts | 21 +++++++++--- .../src/api/explorer/channels.routes.ts | 30 +++++++++++++---- .../src/api/explorer/general.routes.ts | 32 +++++++++++++++++++ .../src/api/explorer/nodes.api.ts | 12 +++++++ .../src/api/explorer/nodes.routes.ts | 10 ++++++ lightning-backend/src/server.ts | 2 ++ 7 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 lightning-backend/src/api/explorer/general.routes.ts diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ad552ffb1..1c439a755 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -240,4 +240,9 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params }); } + lightningSearch$(searchText: string): Observable { + let params = new HttpParams().set('searchText', searchText); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/search', { params }); + } + } diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index be2c953f5..8551a23bd 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -13,6 +13,18 @@ class ChannelsApi { } } + public async $searchChannelsById(search: string): Promise { + 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; + } + } + public async $getChannelsByStatus(status: number): Promise { try { const query = `SELECT * FROM channels WHERE status = ?`; @@ -46,11 +58,12 @@ class ChannelsApi { } } - public async $getChannelByTransactionId(transactionId: string): Promise { + public async $getChannelsByTransactionId(transactionIds: string[]): Promise { try { - 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 = ?`; - const [rows]: any = await DB.query(query, [transactionId]); - return rows[0]; + transactionIds = transactionIds.map((id) => '\'' + id + '\''); + 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(', ')})`; + const [rows]: any = await DB.query(query); + return rows; } catch (e) { logger.err('$getChannelByTransactionId error: ' + (e instanceof Error ? e.message : e)); throw e; diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts index 07dabdd31..61b4846e1 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/lightning-backend/src/api/explorer/channels.routes.ts @@ -8,11 +8,21 @@ class ChannelsRoutes { public initRoutes(app: Express) { app .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) ; } + private async $searchChannelsById(req: Request, res: Response) { + try { + const channels = await channelsApi.$searchChannelsById(req.params.search); + res.json(channels); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getChannel(req: Request, res: Response) { try { const channel = await channelsApi.$getChannel(req.params.short_id); @@ -32,8 +42,8 @@ class ChannelsRoutes { res.status(501).send('Missing parameter: public_key'); return; } - const channels = await channelsApi.$getChannelsForNode(req.query.public_key); - res.json(channels); + const channels = await channelsApi.$getChannelsForNode(req.query.public_key); + res.json(channels); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } @@ -51,12 +61,18 @@ class ChannelsRoutes { txIds.push(req.query.txId[_txId].toString()); } } - const channels: any[] = []; - for (const txId of txIds) { - const channel = await channelsApi.$getChannelByTransactionId(txId); - channels.push(channel); + const channels = await channelsApi.$getChannelsByTransactionId(txIds); + const result: any[] = []; + for (const txid of txIds) { + const foundChannel = channels.find((channel) => channel.transaction_id === txid); + if (foundChannel) { + result.push(foundChannel); + } else { + result.push(null); + } } - res.json(channels); + + res.json(result); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/lightning-backend/src/api/explorer/general.routes.ts b/lightning-backend/src/api/explorer/general.routes.ts new file mode 100644 index 000000000..326602d55 --- /dev/null +++ b/lightning-backend/src/api/explorer/general.routes.ts @@ -0,0 +1,32 @@ +import config from '../../config'; +import { Express, Request, Response } from 'express'; +import nodesApi from './nodes.api'; +import channelsApi from './channels.api'; +class GeneralRoutes { + constructor() { } + + public initRoutes(app: Express) { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels) + ; + } + + private async $searchNodesAndChannels(req: Request, res: Response) { + if (typeof req.query.searchText !== 'string') { + res.status(501).send('Missing parameter: searchText'); + return; + } + try { + const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.query.searchText); + const channels = await channelsApi.$searchChannelsById(req.query.searchText); + res.json({ + nodes: nodes, + channels: channels, + }); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } +} + +export default new GeneralRoutes(); diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index 119336723..e8e18b082 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -59,6 +59,18 @@ class NodesApi { throw e; } } + + public async $searchNodeByPublicKeyOrAlias(search: string) { + try { + const searchStripped = search.replace('%', '') + '%'; + const query = `SELECT public_key, alias, color FROM nodes WHERE public_key LIKE ? OR alias LIKE ? LIMIT 10`; + 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; + } + } } export default new NodesApi(); diff --git a/lightning-backend/src/api/explorer/nodes.routes.ts b/lightning-backend/src/api/explorer/nodes.routes.ts index 1b86abb69..e21617f45 100644 --- a/lightning-backend/src/api/explorer/nodes.routes.ts +++ b/lightning-backend/src/api/explorer/nodes.routes.ts @@ -7,12 +7,22 @@ class NodesRoutes { public initRoutes(app: Express) { app .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/search/:search', this.$searchNode) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats) .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) ; } + private async $searchNode(req: Request, res: Response) { + try { + const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); + res.json(nodes); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getNode(req: Request, res: Response) { try { const node = await nodesApi.$getNode(req.params.public_key); diff --git a/lightning-backend/src/server.ts b/lightning-backend/src/server.ts index 954be4f0a..afee2e070 100644 --- a/lightning-backend/src/server.ts +++ b/lightning-backend/src/server.ts @@ -3,6 +3,7 @@ import * as express from 'express'; import * as http from 'http'; import logger from './logger'; import config from './config'; +import generalRoutes from './api/explorer/general.routes'; import nodesRoutes from './api/explorer/nodes.routes'; import channelsRoutes from './api/explorer/channels.routes'; @@ -30,6 +31,7 @@ class Server { } private initRoutes() { + generalRoutes.initRoutes(this.app); nodesRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app); } From 8604869e5e07193d01a8c1c45ac641e313865dcd Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 15 May 2022 14:47:55 +0400 Subject: [PATCH 17/51] Search bar refactored with Nodes and Channels --- .../app/components/qrcode/qrcode.component.ts | 2 +- .../search-form/search-form.component.html | 5 +- .../search-form/search-form.component.scss | 3 +- .../search-form/search-form.component.ts | 90 +++++++++++-------- .../search-results.component.html | 26 ++++++ .../search-results.component.scss | 16 ++++ .../search-results.component.ts | 68 ++++++++++++++ .../transactions-list.component.ts | 3 +- .../channels-list.component.html | 28 +++--- .../src/app/lightning/node/node.component.ts | 1 - 10 files changed, 187 insertions(+), 55 deletions(-) create mode 100644 frontend/src/app/components/search-form/search-results/search-results.component.html create mode 100644 frontend/src/app/components/search-form/search-results/search-results.component.scss create mode 100644 frontend/src/app/components/search-form/search-results/search-results.component.ts diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index 07f2703fc..923e66fd8 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -21,7 +21,7 @@ export class QrcodeComponent implements AfterViewInit { ) { } ngOnChanges() { - if (!this.canvas.nativeElement) { + if (!this.canvas || !this.canvas.nativeElement) { return; } this.render(); diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 422dfaa62..417414b58 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,7 +1,10 @@
- + + + +
diff --git a/frontend/src/app/components/search-form/search-form.component.scss b/frontend/src/app/components/search-form/search-form.component.scss index f316c3aa7..448cb28b3 100644 --- a/frontend/src/app/components/search-form/search-form.component.scss +++ b/frontend/src/app/components/search-form/search-form.component.scss @@ -32,6 +32,7 @@ form { } .search-box-container { + position: relative; width: 100%; @media (min-width: 768px) { min-width: 400px; @@ -48,4 +49,4 @@ form { .btn { width: 100px; } -} +} \ No newline at end of file diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index d83975c50..3914918ad 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,41 +1,40 @@ -import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild } from '@angular/core'; +import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { AssetsService } from 'src/app/services/assets.service'; import { StateService } from 'src/app/services/state.service'; -import { Observable, of, Subject, merge } from 'rxjs'; +import { Observable, of, Subject, merge, zip } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map } from 'rxjs/operators'; import { ElectrsApiService } from 'src/app/services/electrs-api.service'; -import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { ShortenStringPipe } from 'src/app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; +import { ApiService } from 'src/app/services/api.service'; +import { SearchResultsComponent } from './search-results/search-results.component'; @Component({ selector: 'app-search-form', templateUrl: './search-form.component.html', styleUrls: ['./search-form.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchFormComponent implements OnInit { network = ''; assets: object = {}; isSearching = false; - typeaheadSearchFn: ((text: Observable) => Observable); - + typeAhead$: Observable; searchForm: FormGroup; - isMobile = (window.innerWidth <= 767.98); - @Output() searchTriggered = new EventEmitter(); regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/; regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64}):?(\d+)?$/; regexBlockheight = /^[0-9]+$/; - - @ViewChild('instance', {static: true}) instance: NgbTypeahead; focus$ = new Subject(); click$ = new Subject(); - formatterFn = (address: string) => this.shortenStringPipe.transform(address, this.isMobile ? 33 : 40); + @Output() searchTriggered = new EventEmitter(); + @ViewChild('searchResults') searchResults: SearchResultsComponent; + @HostListener('keydown', ['$event']) keydown($event) { + this.handleKeyDown($event); + } constructor( private formBuilder: FormBuilder, @@ -43,12 +42,11 @@ export class SearchFormComponent implements OnInit { private assetsService: AssetsService, private stateService: StateService, private electrsApiService: ElectrsApiService, + private apiService: ApiService, private relativeUrlPipe: RelativeUrlPipe, - private shortenStringPipe: ShortenStringPipe, ) { } ngOnInit() { - this.typeaheadSearchFn = this.typeaheadSearch; this.stateService.networkChanged$.subscribe((network) => this.network = network); this.searchForm = this.formBuilder.group({ @@ -61,43 +59,63 @@ export class SearchFormComponent implements OnInit { this.assets = assets; }); } - } - typeaheadSearch = (text$: Observable) => { - const debouncedText$ = text$.pipe( - map((text) => { - if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { - return text.substr(1); - } - return text; - }), - debounceTime(200), - distinctUntilChanged() - ); - const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); - const inputFocus$ = this.focus$; - - return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$) + this.typeAhead$ = this.searchForm.get('searchText').valueChanges .pipe( + map((text) => { + if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { + return text.substr(1); + } + return text; + }), + debounceTime(300), + distinctUntilChanged(), switchMap((text) => { if (!text.length) { - return of([]); + return of([ + [], + { + nodes: [], + channels: [], + } + ]); } - return this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))); + return zip( + this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), + this.apiService.lightningSearch$(text), + ); }), - map((result: string[]) => { + map((result: any[]) => { if (this.network === 'bisq') { - return result.map((address: string) => 'B' + address); + return result[0].map((address: string) => 'B' + address); } - return result; + return { + addresses: result[0], + nodes: result[1].nodes, + channels: result[1].channels, + totalResults: result[0].length + result[1].nodes.length + result[1].channels.length, + }; }) ); - } + } + handleKeyDown($event) { + this.searchResults.handleKeyDown($event); + } itemSelected() { setTimeout(() => this.search()); } + selectedResult(result: any) { + if (typeof result === 'string') { + this.navigate('/address/', result); + } else if (result.alias) { + this.navigate('/lightning/node/', result.public_key); + } else if (result.short_id) { + this.navigate('/lightning/channel/', result.id); + } + } + search() { const searchText = this.searchForm.value.searchText.trim(); if (searchText) { diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html new file mode 100644 index 000000000..e3e3e5212 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -0,0 +1,26 @@ + diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.scss b/frontend/src/app/components/search-form/search-results/search-results.component.scss new file mode 100644 index 000000000..094865bb6 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.scss @@ -0,0 +1,16 @@ +.card-title { + color: #4a68b9; + font-size: 10px; + margin-bottom: 4px; + font-size: 1rem; + + margin-left: 10px; +} + +.dropdown-menu { + position: absolute; + top: 42px; + left: 0px; + box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075); + width: 100%; +} diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts new file mode 100644 index 000000000..0ce88fe04 --- /dev/null +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -0,0 +1,68 @@ +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'], +}) +export class SearchResultsComponent implements OnChanges { + @Input() results: any = {}; + @Input() searchTerm = ''; + @Output() selectedResult = new EventEmitter(); + + isMobile = (window.innerWidth <= 767.98); + resultsFlattened = []; + activeIdx = 0; + focusFirst = true; + + constructor() { } + + ngOnChanges() { + this.activeIdx = 0; + if (this.results) { + this.resultsFlattened = [...this.results.addresses, ...this.results.nodes, ...this.results.channels]; + } + } + + handleKeyDown(event: KeyboardEvent) { + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + this.next(); + break; + case 'ArrowUp': + event.preventDefault(); + this.prev(); + break; + case 'Enter': + event.preventDefault(); + this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); + this.results = null; + break; + } + } + + clickItem(id: number) { + this.selectedResult.emit(this.resultsFlattened[id]); + this.results = null; + } + + next() { + if (this.activeIdx === this.resultsFlattened.length - 1) { + this.activeIdx = this.focusFirst ? (this.activeIdx + 1) % this.resultsFlattened.length : -1; + } else { + this.activeIdx++; + } + } + + prev() { + if (this.activeIdx < 0) { + this.activeIdx = this.resultsFlattened.length - 1; + } else if (this.activeIdx === 0) { + this.activeIdx = this.focusFirst ? this.resultsFlattened.length - 1 : -1; + } else { + this.activeIdx--; + } + } + +} diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index ab0a742cf..0ea41a2bb 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -5,7 +5,7 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from 'src/environments/environment'; import { AssetsService } from 'src/app/services/assets.service'; -import { map, tap, switchMap } from 'rxjs/operators'; +import { filter, map, tap, switchMap } from 'rxjs/operators'; import { BlockExtended } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; @@ -78,6 +78,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { ), this.refreshChannels$ .pipe( + filter(() => this.stateService.env.LIGHTNING), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), map((channels) => { this.channels = channels; diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 066d37a70..fe6d44e42 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -3,10 +3,10 @@ - - - - + + + + @@ -15,18 +15,18 @@ - - - @@ -34,22 +34,22 @@ - - - - - - - - +
Node AliasNode IDStatusFee RateCapacityNode IDStatusFee RateCapacity Channel ID
{{ channel.alias_left || '?' }} + {{ channel.node1_public_key | shortenString : 10 }} + Inactive Active Closed + {{ channel.node1_fee_rate / 10000 | number }}% {{ channel.alias_right || '?' }} + {{ channel.node2_public_key | shortenString : 10 }} + Inactive Active Closed + {{ channel.node2_fee_rate / 10000 | number }}% + @@ -66,13 +66,13 @@ + + + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index f3c0d6153..5842a1b9d 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -47,7 +47,6 @@ export class NodeComponent implements OnInit { socket: node.public_key + '@' + socket, }); } - console.log(socketsObject); node.socketsObject = socketsObject; return node; }), From ac10aafc07db942a350abf88a840f4bc5236cdc8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 15 May 2022 16:48:38 +0400 Subject: [PATCH 18/51] More node info --- .../app/lightning/node/node.component.html | 28 ++++++++++++++++--- .../components/sats/sats.component.html | 2 +- .../src/api/explorer/nodes.api.ts | 4 +-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 713cb2709..5309e10f2 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -14,6 +14,30 @@
+
+ + + + + + + + + + + + + + + +
Total capacity +   +
Total channels + {{ node.channel_count }} +
Average channel size +   +
+
@@ -36,10 +60,6 @@
-
-
- -
diff --git a/frontend/src/app/shared/components/sats/sats.component.html b/frontend/src/app/shared/components/sats/sats.component.html index 8358812a9..d8b52b956 100644 --- a/frontend/src/app/shared/components/sats/sats.component.html +++ b/frontend/src/app/shared/components/sats/sats.component.html @@ -1,4 +1,4 @@ -‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }} +‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : '1.0-0' }} L- tL- t- diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index e8e18b082..28aa6d925 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -4,8 +4,8 @@ import DB from '../../database'; class NodesApi { public async $getNode(public_key: string): Promise { try { - const query = `SELECT nodes.*, (SELECT COUNT(*) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS channel_count, (SELECT SUM(capacity) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS capacity FROM nodes WHERE public_key = ?`; - const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key]); + const query = `SELECT nodes.*, (SELECT COUNT(*) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS channel_count, (SELECT SUM(capacity) FROM channels WHERE channels.status < 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)) AS capacity, (SELECT AVG(capacity) FROM channels WHERE status < 2 AND (node1_public_key = ? OR node2_public_key = ?)) AS channels_capacity_avg FROM nodes WHERE public_key = ?`; + const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key]); return rows[0]; } catch (e) { logger.err('$getNode error: ' + (e instanceof Error ? e.message : e)); From 9ebc8813e3e777bb794d06811d0bf8c856d0d5e5 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 15 May 2022 19:22:14 +0400 Subject: [PATCH 19/51] Node and Channel pages improvements --- .../search-form/search-form.component.ts | 9 +- .../search-results.component.html | 4 +- .../channel-box/channel-box.component.html | 40 +++++++ .../channel-box/channel-box.component.scss | 0 .../channel-box/channel-box.component.ts | 14 +++ .../lightning/channel/channel.component.html | 104 +++--------------- .../lightning/channel/channel.component.scss | 40 ++++++- .../lightning/channel/channel.component.ts | 3 + .../channels-list.component.html | 79 +++++-------- .../lightning-dashboard.component.html | 2 +- .../src/app/lightning/lightning.module.ts | 2 + .../app/lightning/node/node.component.html | 11 +- .../app/lightning/node/node.component.scss | 63 ++++++----- .../src/app/lightning/node/node.component.ts | 4 + .../src/api/explorer/channels.api.ts | 47 +++++++- .../src/api/explorer/nodes.api.ts | 2 +- 16 files changed, 239 insertions(+), 185 deletions(-) create mode 100644 frontend/src/app/lightning/channel/channel-box/channel-box.component.html create mode 100644 frontend/src/app/lightning/channel/channel-box/channel-box.component.scss create mode 100644 frontend/src/app/lightning/channel/channel-box/channel-box.component.ts diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 3914918ad..b9f22b0cf 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -66,9 +66,9 @@ export class SearchFormComponent implements OnInit { if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { return text.substr(1); } - return text; + return text.trim(); }), - debounceTime(300), + debounceTime(250), distinctUntilChanged(), switchMap((text) => { if (!text.length) { @@ -82,7 +82,10 @@ export class SearchFormComponent implements OnInit { } return zip( this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - this.apiService.lightningSearch$(text), + this.apiService.lightningSearch$(text).pipe(catchError(() => of({ + nodes: [], + channels: [], + }))), ); }), map((result: any[]) => { diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index e3e3e5212..8c18b1565 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -17,8 +17,8 @@
Lightning Channels
- - diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html new file mode 100644 index 000000000..978d49c8c --- /dev/null +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -0,0 +1,40 @@ +
+

{{ channel.alias || '?' }}

+ + {{ channel.public_key | shortenString : 12 }} + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
Fee rate + {{ channel.fee_rate }} ppm ({{ channel.fee_rate / 10000 | number }}%) +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.ts b/frontend/src/app/lightning/channel/channel-box/channel-box.component.ts new file mode 100644 index 000000000..f6f735f56 --- /dev/null +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.ts @@ -0,0 +1,14 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-channel-box', + templateUrl: './channel-box.component.html', + styleUrls: ['./channel-box.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ChannelBoxComponent { + @Input() channel: any; + + constructor() { } + +} diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 649773b41..47b5b334c 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -1,11 +1,15 @@
-
-

Channel {{ channel.short_id }}

-
- Inactive - Active - Closed -
+
+

{{ channel.short_id }}

+ + {{ channel.id }} + + +
+
+ Inactive + Active + Closed
@@ -42,7 +46,7 @@
Capacity 
@@ -55,90 +59,10 @@
-
-

{{ channel.alias_left || '?' }}

- - {{ channel.node1_public_key | shortenString : 18 }} - - -
-
- -
-
- - - - - - - - - - - - - - - - - - - -
Fee rate - {{ channel.node1_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
-
-
-
+
-
-

{{ channel.alias_right || '?' }}

- - {{ channel.node2_public_key | shortenString : 18 }} - - -
-
- -
- - - - - - - - - - - - - - - - - - - -
Fee rate - {{ channel.node2_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
-
-
+
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss index a6878a23c..a5aff4428 100644 --- a/frontend/src/app/lightning/channel/channel.component.scss +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -1,3 +1,41 @@ +.title-container { + display: flex; + flex-direction: row; + + @media (max-width: 768px) { + flex-direction: column; + } +} + +.tx-link { + display: flex; + flex-grow: 1; + @media (min-width: 650px) { + align-self: end; + margin-left: 15px; + margin-top: 0px; + margin-bottom: -3px; + } + @media (min-width: 768px) { + margin-bottom: 4px; + top: 1px; + position: relative; + } + @media (max-width: 768px) { + order: 2; + } +} + .badges { font-size: 20px; -} \ No newline at end of file +} + +app-fiat { + display: block; + font-size: 13px; + @media (min-width: 768px) { + font-size: 14px; + display: inline-block; + margin-left: 10px; + } +} diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index b64e08353..029b83e08 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; +import { SeoService } from 'src/app/services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -16,12 +17,14 @@ export class ChannelComponent implements OnInit { constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, + private seoService: SeoService, ) { } ngOnInit(): void { this.channel$ = this.activatedRoute.paramMap .pipe( switchMap((params: ParamMap) => { + this.seoService.setTitle(`Channel: ${params.get('short_id')}`); return this.lightningApiService.getChannel$(params.get('short_id')); }) ); diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index fe6d44e42..dfc01a6e5 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -1,60 +1,16 @@
- - + - + - - - - - - - - - - - - - - + @@ -75,12 +31,37 @@ -
Node Alias Node ID StatusFee RateFee Rate CapacityChannel IDChannel ID
- {{ channel.alias_left || '?' }} - - - {{ channel.node1_public_key | shortenString : 10 }} - - - - Inactive - Active - Closed - - {{ channel.node1_fee_rate / 10000 | number }}% - - {{ channel.alias_right || '?' }} - - - {{ channel.node2_public_key | shortenString : 10 }} - - - - Inactive - Active - Closed - - {{ channel.node2_fee_rate / 10000 | number }}% - - - - {{ channel.short_id }} -
+
+
-
\ No newline at end of file + + + {{ node.alias || '?' }} + + + + {{ node.public_key | shortenString : 10 }} + + + + + Inactive + Active + Closed + + + {{ node.fee_rate }} ppm ({{ node.fee_rate / 10000 | number }}%) + + + + + + {{ channel.short_id }} + + diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 3c14763a5..31b0784d6 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -4,7 +4,7 @@
- Nodes Statistics  + Network Statistics 
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index ffb1fd356..1ab421108 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -11,6 +11,7 @@ import { LightningRoutingModule } from './lightning.routing.module'; import { ChannelsListComponent } from './channels-list/channels-list.component'; import { ChannelComponent } from './channel/channel.component'; import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; +import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -20,6 +21,7 @@ import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper ChannelsListComponent, ChannelComponent, LightningWrapperComponent, + ChannelBoxComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 5309e10f2..80e357c22 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -1,10 +1,8 @@
@@ -20,7 +18,7 @@ Total capacity -   + @@ -32,12 +30,13 @@ Average channel size -   +
+
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index c3831ded9..db4adc7b6 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -1,3 +1,31 @@ +.title-container { + display: flex; + flex-direction: row; + + @media (max-width: 768px) { + flex-direction: column; + } +} + +.tx-link { + display: flex; + flex-grow: 1; + @media (min-width: 650px) { + align-self: end; + margin-left: 15px; + margin-top: 0px; + margin-bottom: -3px; + } + @media (min-width: 768px) { + margin-bottom: 4px; + top: 1px; + position: relative; + } + @media (max-width: 768px) { + order: 2; + } +} + .qr-wrapper { background-color: #FFF; padding: 10px; @@ -20,34 +48,13 @@ position: relative; } -.qrcode-col { - margin: 20px auto 10px; - text-align: center; - @media (min-width: 992px){ - margin: 0px auto 0px; +app-fiat { + display: block; + font-size: 13px; + @media (min-width: 768px) { + font-size: 14px; + display: inline-block; + margin-left: 10px; } } -.tx-link { - display: flex; - flex-grow: 1; - @media (min-width: 650px) { - align-self: end; - margin-left: 15px; - margin-top: 0px; - margin-bottom: -3px; - } - @media (min-width: 768px) { - margin-bottom: 4px; - top: 1px; - position: relative; - } - @media (max-width: 768px) { - order: 3; - } -} - -.title-container { - display: flex; - flex-direction: row; -} diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 5842a1b9d..3f2af52b9 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; +import { SeoService } from 'src/app/services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -20,6 +21,7 @@ export class NodeComponent implements OnInit { constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, + private seoService: SeoService, ) { } ngOnInit(): void { @@ -29,6 +31,8 @@ export class NodeComponent implements OnInit { return this.lightningApiService.getNode$(params.get('public_key')); }), map((node) => { + this.seoService.setTitle(`Node: ${node.alias}`); + const socketsObject = []; for (const socket of node.sockets.split(',')) { if (socket === '') { diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index 8551a23bd..283b9388a 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -51,7 +51,9 @@ class ChannelsApi { try { 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.id = ?`; const [rows]: any = await DB.query(query, [shortId]); - return rows[0]; + if (rows[0]) { + return this.convertChannel(rows[0]); + } } catch (e) { logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); throw e; @@ -63,7 +65,8 @@ class ChannelsApi { transactionIds = transactionIds.map((id) => '\'' + id + '\''); 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(', ')})`; const [rows]: any = await DB.query(query); - return rows; + const channels = rows.map((row) => this.convertChannel(row)); + return channels; } catch (e) { logger.err('$getChannelByTransactionId error: ' + (e instanceof Error ? e.message : e)); throw e; @@ -72,14 +75,50 @@ class ChannelsApi { public async $getChannelsForNode(public_key: string): Promise { try { - 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 node1_public_key = ? OR node2_public_key = ?`; + 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 node1_public_key = ? OR node2_public_key = ? ORDER BY channels.capacity DESC`; const [rows]: any = await DB.query(query, [public_key, public_key]); - return rows; + const channels = rows.map((row) => this.convertChannel(row)); + return channels; } catch (e) { logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e)); throw e; } } + + private convertChannel(channel: any): any { + return { + 'id': channel.id, + 'short_id': channel.short_id, + 'capacity': channel.capacity, + 'transaction_id': channel.transaction_id, + 'transaction_vout': channel.void, + 'updated_at': channel.updated_at, + 'created': channel.created, + 'status': channel.status, + 'node_left': { + 'alias': channel.alias_left, + 'public_key': channel.node1_public_key, + '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, + '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, + }, + }; + } } export default new ChannelsApi(); diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index 28aa6d925..391056d0b 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -63,7 +63,7 @@ class NodesApi { public async $searchNodeByPublicKeyOrAlias(search: string) { try { const searchStripped = search.replace('%', '') + '%'; - const query = `SELECT public_key, alias, color FROM nodes WHERE public_key LIKE ? OR alias LIKE ? LIMIT 10`; + 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`; const [rows]: any = await DB.query(query, [searchStripped, searchStripped]); return rows; } catch (e) { From 11a7babbc453113dd56d694edb550fd56ec06330 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 16 May 2022 00:01:53 +0400 Subject: [PATCH 20/51] Improving channels api with node data --- .../clipboard/clipboard.component.html | 4 ++-- .../clipboard/clipboard.component.ts | 1 + .../channel-box/channel-box.component.html | 18 ++++++++++++------ .../channel-box/channel-box.component.scss | 13 +++++++++++++ .../channels-list/channels-list.component.html | 18 +++++++++++------- .../channels-list/channels-list.component.scss | 3 +++ .../src/api/explorer/channels.api.ts | 8 ++++++-- .../src/tasks/stats-updater.service.ts | 2 +- 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/components/clipboard/clipboard.component.html b/frontend/src/app/components/clipboard/clipboard.component.html index 1150074ac..58ba76937 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.html +++ b/frontend/src/app/components/clipboard/clipboard.component.html @@ -1,5 +1,5 @@ - diff --git a/frontend/src/app/components/clipboard/clipboard.component.ts b/frontend/src/app/components/clipboard/clipboard.component.ts index 317cba7b6..7190c9c18 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.ts +++ b/frontend/src/app/components/clipboard/clipboard.component.ts @@ -11,6 +11,7 @@ import * as tlite from 'tlite'; export class ClipboardComponent implements AfterViewInit { @ViewChild('btn') btn: ElementRef; @ViewChild('buttonWrapper') buttonWrapper: ElementRef; + @Input() size: 'small' | 'normal' = 'normal'; @Input() text: string; copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`; diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 978d49c8c..fa9340205 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -1,9 +1,15 @@ -
-

{{ channel.alias || '?' }}

- - {{ channel.public_key | shortenString : 12 }} - - +
+
+

{{ channel.alias || '?' }}

+ + {{ channel.public_key | shortenString : 12 }} + + +
+
+
{{ channel.channels }} channels
+
+
diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss index e69de29bb..1041633f6 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss @@ -0,0 +1,13 @@ +.box-top { + display: flex; +} + +.box-left { + width: 100%; +} + +.box-right { + text-align: right; + width: 50%; + margin-top: auto; +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index dfc01a6e5..72e451b0d 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -2,7 +2,7 @@
- + @@ -42,13 +42,17 @@ - + + + + + + + + - @@ -65,7 +60,37 @@ - + + +

Channels

+ +
Node AliasNode ID  Status Fee Rate Capacity - {{ node.alias || '?' }} +
{{ node.alias || '?' }}
+
- - {{ node.public_key | shortenString : 10 }} - - + +
{{ node.channels }} channels
+
Inactive diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.scss b/frontend/src/app/lightning/channels-list/channels-list.component.scss index e69de29bb..35a6ce0bc 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.scss +++ b/frontend/src/app/lightning/channels-list/channels-list.component.scss @@ -0,0 +1,3 @@ +.second-line { + font-size: 12px; +} \ No newline at end of file diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index 283b9388a..f7b0a5751 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -49,7 +49,7 @@ class ChannelsApi { public async $getChannel(shortId: string): Promise { try { - 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.id = ?`; + 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 = ?`; const [rows]: any = await DB.query(query, [shortId]); if (rows[0]) { return this.convertChannel(rows[0]); @@ -75,7 +75,7 @@ class ChannelsApi { public async $getChannelsForNode(public_key: string): Promise { try { - 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 node1_public_key = ? OR node2_public_key = ? ORDER BY channels.capacity DESC`; + 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 (node1_public_key = ? OR node2_public_key = ?) ORDER BY channels.capacity DESC`; const [rows]: any = await DB.query(query, [public_key, public_key]); const channels = rows.map((row) => this.convertChannel(row)); return channels; @@ -98,6 +98,8 @@ class ChannelsApi { 'node_left': { 'alias': channel.alias_left, 'public_key': channel.node1_public_key, + 'channels': channel.channels_left, + 'capacity': channel.capacity_left, 'base_fee_mtokens': channel.node1_base_fee_mtokens, 'cltv_delta': channel.node1_cltv_delta, 'fee_rate': channel.node1_fee_rate, @@ -109,6 +111,8 @@ class ChannelsApi { 'node_right': { 'alias': channel.alias_right, 'public_key': channel.node2_public_key, + 'channels': channel.channels_right, + 'capacity': channel.capacity_right, 'base_fee_mtokens': channel.node2_base_fee_mtokens, 'cltv_delta': channel.node2_cltv_delta, 'fee_rate': channel.node2_fee_rate, diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index c0a6392b1..ced094a66 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -33,7 +33,7 @@ class LightningStatsUpdater { return; } - 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 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 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`; + 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); // First run we won't have any nodes yet From 473cb55dc4e3674e214ba93c8481817eda1c729b Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 16 May 2022 01:36:59 +0400 Subject: [PATCH 21/51] Channel pagination --- .../address-labels.component.ts | 2 +- .../channel-box/channel-box.component.spec.ts | 25 +++++ .../channels-list.component.html | 99 ++++++++++++------- .../channels-list/channels-list.component.ts | 49 ++++++++- .../app/lightning/lightning-api.service.ts | 6 +- .../app/lightning/node/node.component.html | 1 - .../src/api/explorer/channels.api.ts | 31 +++++- .../src/api/explorer/channels.routes.ts | 11 ++- 8 files changed, 171 insertions(+), 53 deletions(-) create mode 100644 frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index dbac4ab11..f2018bfe5 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -34,7 +34,7 @@ export class AddressLabelsComponent implements OnChanges { } handleChannel() { - this.label = `Channel open: ${this.channel.alias_left} <> ${this.channel.alias_right}`; + this.label = `Channel open: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; } handleVin() { diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts new file mode 100644 index 000000000..ae9463a6c --- /dev/null +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChannelBoxComponent } from './channel-box.component'; + +describe('ChannelBoxComponent', () => { + let component: ChannelBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ChannelBoxComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 72e451b0d..aec19de5f 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -1,44 +1,39 @@ -
+
+

Channels ({{ response.totalItems }})

+ + +
+ + +
+ + - - - - - - - - - - + + + - - - - - - - - - - - -
Node Alias StatusFee RateCapacityChannel ID
- - - - - - - - - - - -
+ +
+ + +
Node Alias StatusFee RateCapacityChannel ID
@@ -50,7 +45,7 @@ +
{{ node.channels }} channels
+ {{ channel.short_id }}
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index f6b0c448b..debf2467a 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs'; +import { map, startWith, switchMap } from 'rxjs/operators'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -8,16 +10,53 @@ import { LightningApiService } from '../lightning-api.service'; styleUrls: ['./channels-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ChannelsListComponent implements OnChanges { +export class ChannelsListComponent implements OnInit, OnChanges { @Input() publicKey: string; - channels$: Observable; + channels$: Observable; + + // @ts-ignore + paginationSize: 'sm' | 'lg' = 'md'; + paginationMaxSize = 10; + itemsPerPage = 25; + page = 1; + channelsPage$ = new BehaviorSubject(1); + channelStatusForm: FormGroup; + defaultStatus = 'open'; constructor( private lightningApiService: LightningApiService, - ) { } + private formBuilder: FormBuilder, + ) { + this.channelStatusForm = this.formBuilder.group({ + status: [this.defaultStatus], + }); + } + + ngOnInit() { + if (document.body.clientWidth < 670) { + this.paginationSize = 'sm'; + this.paginationMaxSize = 3; + } + } ngOnChanges(): void { - this.channels$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey); + this.channels$ = combineLatest([ + this.channelsPage$, + this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus)) + ]) + .pipe( + switchMap(([page, status]) =>this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status)), + map((response) => { + return { + channels: response.body, + totalItems: parseInt(response.headers.get('x-total-count'), 10) + }; + }), + ); + } + + pageChange(page: number) { + this.channelsPage$.next(page); } } diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index a49923546..9197f4f02 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -20,12 +20,14 @@ export class LightningApiService { return this.httpClient.get(API_BASE_URL + '/channels/' + shortId); } - getChannelsByNodeId$(publicKey: string): Observable { + getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable { let params = new HttpParams() .set('public_key', publicKey) + .set('index', index) + .set('status', status) ; - return this.httpClient.get(API_BASE_URL + '/channels', { params }); + return this.httpClient.get(API_BASE_URL + '/channels', { params, observe: 'response' }); } getLatestStatistics$(): Observable { diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 80e357c22..93bb67e61 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -88,7 +88,6 @@

-

Channels

diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index f7b0a5751..95d660bb4 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -73,10 +73,16 @@ class ChannelsApi { } } - public async $getChannelsForNode(public_key: string): Promise { + public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise { try { - 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 (node1_public_key = ? OR node2_public_key = ?) ORDER BY channels.capacity DESC`; - const [rows]: any = await DB.query(query, [public_key, public_key]); + // Default active and inactive channels + let statusQuery = '< 2'; + // Closed channels only + if (status === 'closed') { + statusQuery = '= 2'; + } + 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 (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery} ORDER BY channels.capacity DESC LIMIT ?, ?`; + const [rows]: any = await DB.query(query, [public_key, public_key, index, length]); const channels = rows.map((row) => this.convertChannel(row)); return channels; } catch (e) { @@ -85,13 +91,30 @@ class ChannelsApi { } } + public async $getChannelsCountForNode(public_key: string, status: string): Promise { + try { + // Default active and inactive channels + let statusQuery = '< 2'; + // Closed channels only + if (status === 'closed') { + statusQuery = '= 2'; + } + const query = `SELECT COUNT(*) AS count FROM channels WHERE (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery}`; + 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; + } + } + private convertChannel(channel: any): any { return { 'id': channel.id, 'short_id': channel.short_id, 'capacity': channel.capacity, 'transaction_id': channel.transaction_id, - 'transaction_vout': channel.void, + 'transaction_vout': channel.transaction_vout, 'updated_at': channel.updated_at, 'created': channel.created, 'status': channel.status, diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts index 61b4846e1..fd3b3fac0 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/lightning-backend/src/api/explorer/channels.routes.ts @@ -10,7 +10,7 @@ class ChannelsRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById) .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) - .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode) ; } @@ -36,13 +36,18 @@ class ChannelsRoutes { } } - private async $getChannels(req: Request, res: Response) { + private async $getChannelsForNode(req: Request, res: Response) { try { if (typeof req.query.public_key !== 'string') { res.status(501).send('Missing parameter: public_key'); return; } - const channels = await channelsApi.$getChannelsForNode(req.query.public_key); + const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0; + const status: string = typeof req.query.status === 'string' ? req.query.status : ''; + const length = 25; + const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, length, status); + const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status); + res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); From b87308e14f54e71a68979d1d86d656e78329e063 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 24 May 2022 18:40:59 +0400 Subject: [PATCH 22/51] Merge conflict fix. --- frontend/src/app/app-routing.module.ts | 4 ---- frontend/src/app/shared/shared.module.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index dfcc7aec7..564b8653b 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -20,7 +20,6 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass import { AssetsComponent } from './components/assets/assets.component'; import { AssetComponent } from './components/asset/asset.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; -import { LightningDashboardComponent } from './lightning/lightning-dashboard/lightning-dashboard.component'; let routes: Routes = [ { @@ -99,7 +98,6 @@ let routes: Routes = [ }, { path: 'lightning', - component: LightningDashboardComponent, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], @@ -194,7 +192,6 @@ let routes: Routes = [ }, { path: 'lightning', - component: LightningDashboardComponent, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], @@ -286,7 +283,6 @@ let routes: Routes = [ }, { path: 'lightning', - component: LightningDashboardComponent, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 7e8914000..bbcba71c0 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -4,7 +4,7 @@ import { NgbCollapse, NgbCollapseModule, NgbRadioGroup, NgbTypeaheadModule } fro import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, - faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload } from '@fortawesome/free-solid-svg-icons'; + faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MasterPageComponent } from '../components/master-page/master-page.component'; import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component'; @@ -75,6 +75,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index import { SvgImagesComponent } from '../components/svg-images/svg-images.component'; import { ChangeComponent } from '../components/change/change.component'; import { SatsComponent } from './components/sats/sats.component'; +import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; @NgModule({ declarations: [ @@ -144,6 +145,7 @@ import { SatsComponent } from './components/sats/sats.component'; SvgImagesComponent, ChangeComponent, SatsComponent, + SearchResultsComponent, ], imports: [ CommonModule, @@ -240,7 +242,8 @@ import { SatsComponent } from './components/sats/sats.component'; IndexingProgressComponent, SvgImagesComponent, ChangeComponent, - SatsComponent + SatsComponent, + SearchResultsComponent, ] }) export class SharedModule { @@ -279,5 +282,6 @@ export class SharedModule { library.addIcons(faBook); library.addIcons(faListUl); library.addIcons(faDownload); + library.addIcons(faQrcode); } } From f0ad38dec6ccd3dcd4dd7846df94ca957bb336c1 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 26 May 2022 18:23:57 +0400 Subject: [PATCH 23/51] Search fix. Channel update. --- .../src/app/components/search-form/search-form.component.ts | 2 +- .../search-form/search-results/search-results.component.ts | 6 +++++- .../channel/channel-box/channel-box.component.html | 2 +- .../lightning-wrapper/lightning-wrapper.component.ts | 6 +++++- lightning-backend/src/api/explorer/channels.api.ts | 4 ++-- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index b9f22b0cf..d34659f8f 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -111,7 +111,7 @@ export class SearchFormComponent implements OnInit { selectedResult(result: any) { if (typeof result === 'string') { - this.navigate('/address/', result); + this.search(); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); } else if (result.short_id) { diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts index 0ce88fe04..4c8571187 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.ts +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -36,7 +36,11 @@ export class SearchResultsComponent implements OnChanges { break; case 'Enter': event.preventDefault(); - this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); + if (this.resultsFlattened[this.activeIdx]) { + this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); + } else { + this.selectedResult.emit(this.searchTerm); + } this.results = null; break; } diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index fa9340205..04471d351 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -1,6 +1,6 @@
-

{{ channel.alias || '?' }}

+

{{ channel.alias || '?' }}

{{ channel.public_key | shortenString : 12 }} diff --git a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts index 40bcc83bc..c38a99fde 100644 --- a/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts +++ b/frontend/src/app/lightning/lightning-wrapper/lightning-wrapper.component.ts @@ -7,10 +7,14 @@ import { WebsocketService } from 'src/app/services/websocket.service'; styleUrls: ['./lightning-wrapper.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LightningWrapperComponent { +export class LightningWrapperComponent implements OnInit { constructor( private websocketService: WebsocketService, ) { } + ngOnInit() { + this.websocketService.want(['blocks']); + } + } diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index 95d660bb4..64f6569bf 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -47,10 +47,10 @@ class ChannelsApi { } } - public async $getChannel(shortId: string): Promise { + public async $getChannel(id: string): Promise { try { 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 = ?`; - const [rows]: any = await DB.query(query, [shortId]); + const [rows]: any = await DB.query(query, [id]); if (rows[0]) { return this.convertChannel(rows[0]); } From 4bb23cf0c852fab0d4e0fd5b4c5f5240642bce1e Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 29 Jun 2022 23:06:13 +0200 Subject: [PATCH 24/51] Closed channels forensics --- .../lightning/channel/channel.component.html | 27 +- .../lightning/channel/channel.component.ts | 15 +- .../closing-type/closing-type.component.html | 1 + .../closing-type/closing-type.component.scss | 0 .../closing-type/closing-type.component.ts | 37 +++ .../channels-list.component.html | 7 +- .../src/app/lightning/lightning.module.ts | 2 + lightning-backend/mempool-config.sample.json | 3 + .../bitcoin/bitcoin-api-abstract-factory.ts | 25 ++ .../src/api/bitcoin/bitcoin-api-factory.ts | 15 + .../src/api/bitcoin/bitcoin-api.interface.ts | 175 ++++++++++ .../src/api/bitcoin/bitcoin-api.ts | 313 ++++++++++++++++++ .../src/api/bitcoin/esplora-api.interface.ts | 172 ++++++++++ .../src/api/bitcoin/esplora-api.ts | 84 +++++ .../src/api/explorer/channels.api.ts | 13 + lightning-backend/src/config.ts | 8 + lightning-backend/src/database-migration.ts | 2 + .../src/tasks/node-sync.service.ts | 131 +++++++- 18 files changed, 1019 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/lightning/channel/closing-type/closing-type.component.html create mode 100644 frontend/src/app/lightning/channel/closing-type/closing-type.component.scss create mode 100644 frontend/src/app/lightning/channel/closing-type/closing-type.component.ts create mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts create mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts create mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts create mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api.ts create mode 100644 lightning-backend/src/api/bitcoin/esplora-api.interface.ts create mode 100644 lightning-backend/src/api/bitcoin/esplora-api.ts diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 47b5b334c..43dafd24e 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -29,7 +29,7 @@ - Transaction ID + Opening transaction {{ channel.transaction_id | shortenString : 10 }} @@ -37,6 +37,23 @@ + + + Closing transaction + + + {{ channel.closing_transaction_id | shortenString : 10 }} + + + + + + Closing type + + + + +
@@ -69,3 +86,11 @@

+ + +
+ Error loading data. +

+ {{ error.status }}: {{ error.error }} +
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel.component.ts b/frontend/src/app/lightning/channel/channel.component.ts index 029b83e08..bc66f7180 100644 --- a/frontend/src/app/lightning/channel/channel.component.ts +++ b/frontend/src/app/lightning/channel/channel.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { catchError, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @@ -13,6 +13,7 @@ import { LightningApiService } from '../lightning-api.service'; }) export class ChannelComponent implements OnInit { channel$: Observable; + error: any = null; constructor( private lightningApiService: LightningApiService, @@ -24,8 +25,16 @@ export class ChannelComponent implements OnInit { this.channel$ = this.activatedRoute.paramMap .pipe( switchMap((params: ParamMap) => { + this.error = null; this.seoService.setTitle(`Channel: ${params.get('short_id')}`); - return this.lightningApiService.getChannel$(params.get('short_id')); + return this.lightningApiService.getChannel$(params.get('short_id')) + .pipe( + catchError((err) => { + this.error = err; + console.log(this.error); + return of(null); + }) + ); }) ); } diff --git a/frontend/src/app/lightning/channel/closing-type/closing-type.component.html b/frontend/src/app/lightning/channel/closing-type/closing-type.component.html new file mode 100644 index 000000000..60461d1c0 --- /dev/null +++ b/frontend/src/app/lightning/channel/closing-type/closing-type.component.html @@ -0,0 +1 @@ +{{ label.label }} \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/closing-type/closing-type.component.scss b/frontend/src/app/lightning/channel/closing-type/closing-type.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/channel/closing-type/closing-type.component.ts b/frontend/src/app/lightning/channel/closing-type/closing-type.component.ts new file mode 100644 index 000000000..5aa6158d3 --- /dev/null +++ b/frontend/src/app/lightning/channel/closing-type/closing-type.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-closing-type', + templateUrl: './closing-type.component.html', + styleUrls: ['./closing-type.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ClosingTypeComponent implements OnChanges { + @Input() type = 0; + label: { label: string; class: string }; + + ngOnChanges() { + this.label = this.getLabelFromType(this.type); + } + + getLabelFromType(type: number): { label: string; class: string } { + switch (type) { + case 1: return { + label: 'Mutually closed', + class: 'success', + }; + case 2: return { + label: 'Force closed', + class: 'warning', + }; + case 3: return { + label: 'Force closed with penalty', + class: 'danger', + }; + default: return { + label: 'Unknown', + class: 'secondary', + }; + } + } +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index aec19de5f..a6d553ef1 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -52,7 +52,12 @@ Inactive Active - Closed + + Closed + + + + {{ node.fee_rate }} ppm ({{ node.fee_rate / 10000 | number }}%) diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 1ab421108..a8cad3dc9 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -12,6 +12,7 @@ import { ChannelsListComponent } from './channels-list/channels-list.component'; import { ChannelComponent } from './channel/channel.component'; import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; +import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -22,6 +23,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component ChannelComponent, LightningWrapperComponent, ChannelBoxComponent, + ClosingTypeComponent, ], imports: [ CommonModule, diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index eac34ddf4..9402ff2c2 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -6,6 +6,9 @@ "API_URL_PREFIX": "/api/v1/", "STDOUT_LOG_MIN_PRIORITY": "debug" }, + "ESPLORA": { + "REST_API_URL": "" + }, "SYSLOG": { "ENABLED": false, "HOST": "127.0.0.1", diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts new file mode 100644 index 000000000..cd60843f3 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -0,0 +1,25 @@ +import { IEsploraApi } from './esplora-api.interface'; + +export interface AbstractBitcoinApi { + $getRawMempool(): Promise; + $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; + $getBlockHeightTip(): Promise; + $getTxIdsForBlock(hash: string): Promise; + $getBlockHash(height: number): Promise; + $getBlockHeader(hash: string): Promise; + $getBlock(hash: string): Promise; + $getAddress(address: string): Promise; + $getAddressTransactions(address: string, lastSeenTxId: string): Promise; + $getAddressPrefix(prefix: string): string[]; + $sendRawTransaction(rawTransaction: string): Promise; + $getOutspend(txId: string, vout: number): Promise; + $getOutspends(txId: string): Promise; + $getBatchedOutspends(txId: string[]): Promise; +} +export interface BitcoinRpcCredentials { + host: string; + port: number; + user: string; + pass: string; + timeout: number; +} diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts b/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts new file mode 100644 index 000000000..3ae598ac2 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts @@ -0,0 +1,15 @@ +import config from '../../config'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import EsploraApi from './esplora-api'; +import BitcoinApi from './bitcoin-api'; +import bitcoinClient from './bitcoin-client'; + +function bitcoinApiFactory(): AbstractBitcoinApi { + if (config.ESPLORA.REST_API_URL) { + return new EsploraApi(); + } else { + return new BitcoinApi(bitcoinClient); + } +} + +export default bitcoinApiFactory(); diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts b/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts new file mode 100644 index 000000000..54d666794 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -0,0 +1,175 @@ +export namespace IBitcoinApi { + export interface MempoolInfo { + loaded: boolean; // (boolean) True if the mempool is fully loaded + size: number; // (numeric) Current tx count + bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141. + usage: number; // (numeric) Total memory usage for the mempool + total_fee: number; // (numeric) Total fees of transactions in the mempool + maxmempool: number; // (numeric) Maximum memory usage for the mempool + mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted. + minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions + } + + export interface RawMempool { [txId: string]: MempoolEntry; } + + export interface MempoolEntry { + vsize: number; // (numeric) virtual transaction size as defined in BIP 141. + weight: number; // (numeric) transaction weight as defined in BIP 141. + time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT + height: number; // (numeric) block height when transaction entered pool + descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one) + descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one) + ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one) + ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one) + wtxid: string; // (string) hash of serialized transactionumber; including witness data + fees: { + base: number; // (numeric) transaction fee in BTC + modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC + ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC + descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC + }; + depends: string[]; // (string) parent transaction id + spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction + 'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee) + } + + export interface Block { + hash: string; // (string) the block hash (same as provided) + confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain + size: number; // (numeric) The block size + strippedsize: number; // (numeric) The block size excluding witness data + weight: number; // (numeric) The block weight as defined in BIP 141 + height: number; // (numeric) The block height or index + version: number; // (numeric) The block version + versionHex: string; // (string) The block version formatted in hexadecimal + merkleroot: string; // (string) The merkle root + tx: Transaction[]; + time: number; // (numeric) The block time expressed in UNIX epoch time + mediantime: number; // (numeric) The median block time expressed in UNIX epoch time + nonce: number; // (numeric) The nonce + bits: string; // (string) The bits + difficulty: number; // (numeric) The difficulty + chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex) + nTx: number; // (numeric) The number of transactions in the block + previousblockhash: string; // (string) The hash of the previous block + nextblockhash: string; // (string) The hash of the next block + } + + export interface Transaction { + in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not + hex: string; // (string) The serialized, hex-encoded data for 'txid' + txid: string; // (string) The transaction id (same as provided) + hash: string; // (string) The transaction hash (differs from txid for witness transactions) + size: number; // (numeric) The serialized transaction size + vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions) + weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4) + version: number; // (numeric) The version + locktime: number; // (numeric) The lock time + vin: Vin[]; + vout: Vout[]; + blockhash: string; // (string) the block hash + confirmations: number; // (numeric) The confirmations + blocktime: number; // (numeric) The block time expressed in UNIX epoch time + time: number; // (numeric) Same as blocktime + } + + export interface VerboseBlock extends Block { + tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result + } + + export interface VerboseTransaction extends Transaction { + fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available + } + + export interface Vin { + txid?: string; // (string) The transaction id + vout?: number; // (string) + scriptSig?: { // (json object) The script + asm: string; // (string) asm + hex: string; // (string) hex + }; + sequence: number; // (numeric) The script sequence number + txinwitness?: string[]; // (string) hex-encoded witness data + coinbase?: string; + is_pegin?: boolean; // (boolean) Elements peg-in + } + + export interface Vout { + value: number; // (numeric) The value in BTC + n: number; // (numeric) index + asset?: string; // (string) Elements asset id + scriptPubKey: { // (json object) + asm: string; // (string) the asm + hex: string; // (string) the hex + reqSigs?: number; // (numeric) The required sigs + type: string; // (string) The type, eg 'pubkeyhash' + address?: string; // (string) bitcoin address + addresses?: string[]; // (string) bitcoin addresses + pegout_chain?: string; // (string) Elements peg-out chain + pegout_addresses?: string[]; // (string) Elements peg-out addresses + }; + } + + export interface AddressInformation { + isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned. + isvalid_parent?: boolean; // (boolean) Elements only + address: string; // (string) The bitcoin address validated + scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address + isscript: boolean; // (boolean) If the key is a script + iswitness: boolean; // (boolean) If the address is a witness + witness_version?: number; // (numeric, optional) The version number of the witness program + witness_program: string; // (string, optional) The hex value of the witness program + confidential_key?: string; // (string) Elements only + unconfidential?: string; // (string) Elements only + } + + export interface ChainTips { + height: number; // (numeric) height of the chain tip + hash: string; // (string) block hash of the tip + branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain + status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active'; + } + + export interface BlockchainInfo { + chain: number; // (string) current network name as defined in BIP70 (main, test, regtest) + blocks: number; // (numeric) the current number of blocks processed in the server + headers: number; // (numeric) the current number of headers we have validated + bestblockhash: string, // (string) the hash of the currently best block + difficulty: number; // (numeric) the current difficulty + mediantime: number; // (numeric) median time for the current best block + verificationprogress: number; // (numeric) estimate of verification progress [0..1] + initialblockdownload: boolean; // (bool) (debug information) estimate of whether this node is in Initial Block Download mode. + chainwork: string // (string) total amount of work in active chain, in hexadecimal + size_on_disk: number; // (numeric) the estimated size of the block and undo files on disk + pruned: number; // (boolean) if the blocks are subject to pruning + pruneheight: number; // (numeric) lowest-height complete block stored (only present if pruning is enabled) + automatic_pruning: number; // (boolean) whether automatic pruning is enabled (only present if pruning is enabled) + prune_target_size: number; // (numeric) the target size used by pruning (only present if automatic pruning is enabled) + softforks: SoftFork[]; // (array) status of softforks in progress + bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress + warnings: string; // (string) any network and blockchain warnings. + } + + interface SoftFork { + id: string; // (string) name of softfork + version: number; // (numeric) block version + reject: { // (object) progress toward rejecting pre-softfork blocks + status: boolean; // (boolean) true if threshold reached + }, + } + interface Bip9SoftForks { + status: number; // (string) one of defined, started, locked_in, active, failed + bit: number; // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status) + startTime: number; // (numeric) the minimum median time past of a block at which the bit gains its meaning + timeout: number; // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in + since: number; // (numeric) height of the first block to which the status applies + statistics: { // (object) numeric statistics about BIP9 signalling for a softfork (only for started status) + period: number; // (numeric) the length in blocks of the BIP9 signalling period + threshold: number; // (numeric) the number of blocks with the version bit set required to activate the feature + elapsed: number; // (numeric) the number of blocks elapsed since the beginning of the current period + count: number; // (numeric) the number of blocks with the version bit set in the current period + possible: boolean; // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold + } + } + +} diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api.ts b/lightning-backend/src/api/bitcoin/bitcoin-api.ts new file mode 100644 index 000000000..d8fa07e80 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-api.ts @@ -0,0 +1,313 @@ +import * as bitcoinjs from 'bitcoinjs-lib'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import { IBitcoinApi } from './bitcoin-api.interface'; +import { IEsploraApi } from './esplora-api.interface'; + +class BitcoinApi implements AbstractBitcoinApi { + protected bitcoindClient: any; + + constructor(bitcoinClient: any) { + this.bitcoindClient = bitcoinClient; + } + + $getAddressPrefix(prefix: string): string[] { + throw new Error('Method not implemented.'); + } + + $getBlock(hash: string): Promise { + throw new Error('Method not implemented.'); + } + + $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise { + return this.bitcoindClient.getRawTransaction(txId, true) + .then((transaction: IBitcoinApi.Transaction) => { + if (skipConversion) { + transaction.vout.forEach((vout) => { + vout.value = Math.round(vout.value * 100000000); + }); + return transaction; + } + return this.$convertTransaction(transaction, addPrevout, lazyPrevouts); + }) + .catch((e: Error) => { + throw e; + }); + } + + $getBlockHeightTip(): Promise { + return this.bitcoindClient.getChainTips() + .then((result: IBitcoinApi.ChainTips[]) => { + return result.find(tip => tip.status === 'active')!.height; + }); + } + + $getTxIdsForBlock(hash: string): Promise { + return this.bitcoindClient.getBlock(hash, 1) + .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); + } + + $getRawBlock(hash: string): Promise { + return this.bitcoindClient.getBlock(hash, 0); + } + + $getBlockHash(height: number): Promise { + return this.bitcoindClient.getBlockHash(height); + } + + $getBlockHeader(hash: string): Promise { + return this.bitcoindClient.getBlockHeader(hash, false); + } + + $getAddress(address: string): Promise { + throw new Error('Method getAddress not supported by the Bitcoin RPC API.'); + } + + $getAddressTransactions(address: string, lastSeenTxId: string): Promise { + throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.'); + } + + $getRawMempool(): Promise { + return this.bitcoindClient.getRawMemPool(); + } + + $sendRawTransaction(rawTransaction: string): Promise { + return this.bitcoindClient.sendRawTransaction(rawTransaction); + } + + async $getOutspend(txId: string, vout: number): Promise { + const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); + return { + spent: txOut === null, + status: { + confirmed: true, + } + }; + } + + async $getOutspends(txId: string): Promise { + const outSpends: IEsploraApi.Outspend[] = []; + const tx = await this.$getRawTransaction(txId, true, false); + for (let i = 0; i < tx.vout.length; i++) { + if (tx.status && tx.status.block_height === 0) { + outSpends.push({ + spent: false + }); + } else { + const txOut = await this.bitcoindClient.getTxOut(txId, i); + outSpends.push({ + spent: txOut === null, + }); + } + } + return outSpends; + } + + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; + } + + $getEstimatedHashrate(blockHeight: number): Promise { + // 120 is the default block span in Core + return this.bitcoindClient.getNetworkHashPs(120, blockHeight); + } + + protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise { + let esploraTransaction: IEsploraApi.Transaction = { + txid: transaction.txid, + version: transaction.version, + locktime: transaction.locktime, + size: transaction.size, + weight: transaction.weight, + fee: 0, + vin: [], + vout: [], + status: { confirmed: false }, + }; + + esploraTransaction.vout = transaction.vout.map((vout) => { + return { + value: Math.round(vout.value * 100000000), + scriptpubkey: vout.scriptPubKey.hex, + scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address + : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '', + scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '', + scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), + }; + }); + + // @ts-ignore + esploraTransaction.vin = transaction.vin.map((vin) => { + return { + is_coinbase: !!vin.coinbase, + prevout: null, + scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '', + scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '', + sequence: vin.sequence, + txid: vin.txid || '', + vout: vin.vout || 0, + witness: vin.txinwitness, + }; + }); + + if (transaction.confirmations) { + esploraTransaction.status = { + confirmed: true, + block_height: -1, + block_hash: transaction.blockhash, + block_time: transaction.blocktime, + }; + } + + if (addPrevout) { + esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts); + } else if (!transaction.confirmations) { + // esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction); + } + + return esploraTransaction; + } + + private translateScriptPubKeyType(outputType: string): string { + const map = { + 'pubkey': 'p2pk', + 'pubkeyhash': 'p2pkh', + 'scripthash': 'p2sh', + 'witness_v0_keyhash': 'v0_p2wpkh', + 'witness_v0_scripthash': 'v0_p2wsh', + 'witness_v1_taproot': 'v1_p2tr', + 'nonstandard': 'nonstandard', + 'multisig': 'multisig', + 'nulldata': 'op_return' + }; + + if (map[outputType]) { + return map[outputType]; + } else { + return 'unknown'; + } + } + + private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise { + if (transaction.vin[0].is_coinbase) { + transaction.fee = 0; + return transaction; + } + let totalIn = 0; + + for (let i = 0; i < transaction.vin.length; i++) { + if (lazyPrevouts && i > 12) { + transaction.vin[i].lazy = true; + continue; + } + const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false); + transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout]; + this.addInnerScriptsToVin(transaction.vin[i]); + totalIn += innerTx.vout[transaction.vin[i].vout].value; + } + if (lazyPrevouts && transaction.vin.length > 12) { + transaction.fee = -1; + } else { + const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0); + transaction.fee = parseFloat((totalIn - totalOut).toFixed(8)); + } + return transaction; + } + + private convertScriptSigAsm(hex: string): string { + const buf = Buffer.from(hex, 'hex'); + + const b: string[] = []; + + let i = 0; + while (i < buf.length) { + const op = buf[i]; + if (op >= 0x01 && op <= 0x4e) { + i++; + let push: number; + if (op === 0x4c) { + push = buf.readUInt8(i); + b.push('OP_PUSHDATA1'); + i += 1; + } else if (op === 0x4d) { + push = buf.readUInt16LE(i); + b.push('OP_PUSHDATA2'); + i += 2; + } else if (op === 0x4e) { + push = buf.readUInt32LE(i); + b.push('OP_PUSHDATA4'); + i += 4; + } else { + push = op; + b.push('OP_PUSHBYTES_' + push); + } + + const data = buf.slice(i, i + push); + if (data.length !== push) { + break; + } + + b.push(data.toString('hex')); + i += data.length; + } else { + if (op === 0x00) { + b.push('OP_0'); + } else if (op === 0x4f) { + b.push('OP_PUSHNUM_NEG1'); + } else if (op === 0xb1) { + b.push('OP_CLTV'); + } else if (op === 0xb2) { + b.push('OP_CSV'); + } else if (op === 0xba) { + b.push('OP_CHECKSIGADD'); + } else { + const opcode = bitcoinjs.script.toASM([ op ]); + if (opcode && op < 0xfd) { + if (/^OP_(\d+)$/.test(opcode)) { + b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1')); + } else { + b.push(opcode); + } + } else { + b.push('OP_RETURN_' + op); + } + } + i += 1; + } + } + + return b.join(' '); + } + + private addInnerScriptsToVin(vin: IEsploraApi.Vin): void { + if (!vin.prevout) { + return; + } + + if (vin.prevout.scriptpubkey_type === 'p2sh') { + const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; + vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript); + if (vin.witness && vin.witness.length > 2) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); + } + } + + if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); + } + + if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) { + const witnessScript = vin.witness[vin.witness.length - 2]; + vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); + } + } + +} + +export default BitcoinApi; diff --git a/lightning-backend/src/api/bitcoin/esplora-api.interface.ts b/lightning-backend/src/api/bitcoin/esplora-api.interface.ts new file mode 100644 index 000000000..39f8cfd6f --- /dev/null +++ b/lightning-backend/src/api/bitcoin/esplora-api.interface.ts @@ -0,0 +1,172 @@ +export namespace IEsploraApi { + export interface Transaction { + txid: string; + version: number; + locktime: number; + size: number; + weight: number; + fee: number; + vin: Vin[]; + vout: Vout[]; + status: Status; + hex?: string; + } + + export interface Recent { + txid: string; + fee: number; + vsize: number; + value: number; + } + + export interface Vin { + txid: string; + vout: number; + is_coinbase: boolean; + scriptsig: string; + scriptsig_asm: string; + inner_redeemscript_asm: string; + inner_witnessscript_asm: string; + sequence: any; + witness: string[]; + prevout: Vout | null; + // Elements + is_pegin?: boolean; + issuance?: Issuance; + // Custom + lazy?: boolean; + } + + interface Issuance { + asset_id: string; + is_reissuance: string; + asset_blinding_nonce: string; + asset_entropy: string; + contract_hash: string; + assetamount?: number; + assetamountcommitment?: string; + tokenamount?: number; + tokenamountcommitment?: string; + } + + export interface Vout { + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_type: string; + scriptpubkey_address: string; + value: number; + // Elements + valuecommitment?: number; + asset?: string; + pegout?: Pegout; + } + + interface Pegout { + genesis_hash: string; + scriptpubkey: string; + scriptpubkey_asm: string; + scriptpubkey_address: string; + } + + export interface Status { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + } + + export interface Block { + id: string; + height: number; + version: number; + timestamp: number; + bits: number; + nonce: number; + difficulty: number; + merkle_root: string; + tx_count: number; + size: number; + weight: number; + previousblockhash: string; + } + + export interface Address { + address: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; + electrum?: boolean; + } + + export interface ChainStats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + } + + export interface MempoolStats { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + } + + export interface Outspend { + spent: boolean; + txid?: string; + vin?: number; + status?: Status; + } + + export interface Asset { + asset_id: string; + issuance_txin: IssuanceTxin; + issuance_prevout: IssuancePrevout; + reissuance_token: string; + contract_hash: string; + status: Status; + chain_stats: AssetStats; + mempool_stats: AssetStats; + } + + export interface AssetExtended extends Asset { + name: string; + ticker: string; + precision: number; + entity: Entity; + version: number; + issuer_pubkey: string; + } + + export interface Entity { + domain: string; + } + + interface IssuanceTxin { + txid: string; + vin: number; + } + + interface IssuancePrevout { + txid: string; + vout: number; + } + + interface AssetStats { + tx_count: number; + issuance_count: number; + issued_amount: number; + burned_amount: number; + has_blinded_issuances: boolean; + reissuance_tokens: number; + burned_reissuance_tokens: number; + peg_in_count: number; + peg_in_amount: number; + peg_out_count: number; + peg_out_amount: number; + burn_count: number; + } + +} diff --git a/lightning-backend/src/api/bitcoin/esplora-api.ts b/lightning-backend/src/api/bitcoin/esplora-api.ts new file mode 100644 index 000000000..6ed48a0f8 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/esplora-api.ts @@ -0,0 +1,84 @@ +import config from '../../config'; +import axios, { AxiosRequestConfig } from 'axios'; +import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; +import { IEsploraApi } from './esplora-api.interface'; + +class ElectrsApi implements AbstractBitcoinApi { + axiosConfig: AxiosRequestConfig = { + timeout: 10000, + }; + + constructor() { } + + $getRawMempool(): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig) + .then((response) => response.data); + } + + $getRawTransaction(txId: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) + .then((response) => response.data); + } + + $getBlockHeightTip(): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) + .then((response) => response.data); + } + + $getTxIdsForBlock(hash: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) + .then((response) => response.data); + } + + $getBlockHash(height: number): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig) + .then((response) => response.data); + } + + $getBlockHeader(hash: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig) + .then((response) => response.data); + } + + $getBlock(hash: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig) + .then((response) => response.data); + } + + $getAddress(address: string): Promise { + throw new Error('Method getAddress not implemented.'); + } + + $getAddressTransactions(address: string, txId?: string): Promise { + throw new Error('Method getAddressTransactions not implemented.'); + } + + $getAddressPrefix(prefix: string): string[] { + throw new Error('Method not implemented.'); + } + + $sendRawTransaction(rawTransaction: string): Promise { + throw new Error('Method not implemented.'); + } + + $getOutspend(txId: string, vout: number): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) + .then((response) => response.data); + } + + $getOutspends(txId: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) + .then((response) => response.data); + } + + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; + } +} + +export default ElectrsApi; diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index 64f6569bf..d2dd930c3 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -36,6 +36,17 @@ class ChannelsApi { } } + public async $getClosedChannelsWithoutReason(): Promise { + try { + const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getClosedChannelsWithoutReason error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannelsWithoutCreatedDate(): Promise { try { const query = `SELECT * FROM channels WHERE created IS NULL`; @@ -115,6 +126,8 @@ class ChannelsApi { 'capacity': channel.capacity, 'transaction_id': channel.transaction_id, 'transaction_vout': channel.transaction_vout, + 'closing_transaction_id': channel.closing_transaction_id, + 'closing_reason': channel.closing_reason, 'updated_at': channel.updated_at, 'created': channel.created, 'status': channel.status, diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index 9b71dd977..d2edad180 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -8,6 +8,9 @@ interface IConfig { API_URL_PREFIX: string; STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; }; + ESPLORA: { + REST_API_URL: string; + }; SYSLOG: { ENABLED: boolean; HOST: string; @@ -43,6 +46,9 @@ const defaults: IConfig = { 'API_URL_PREFIX': '/api/v1/', 'STDOUT_LOG_MIN_PRIORITY': 'debug', }, + 'ESPLORA': { + 'REST_API_URL': 'http://127.0.0.1:3000', + }, 'SYSLOG': { 'ENABLED': true, 'HOST': '127.0.0.1', @@ -72,6 +78,7 @@ const defaults: IConfig = { class Config implements IConfig { MEMPOOL: IConfig['MEMPOOL']; + ESPLORA: IConfig['ESPLORA']; SYSLOG: IConfig['SYSLOG']; LN_NODE_AUTH: IConfig['LN_NODE_AUTH']; CORE_RPC: IConfig['CORE_RPC']; @@ -80,6 +87,7 @@ class Config implements IConfig { constructor() { const configs = this.merge(configFile, defaults); this.MEMPOOL = configs.MEMPOOL; + this.ESPLORA = configs.ESPLORA; this.SYSLOG = configs.SYSLOG; this.LN_NODE_AUTH = configs.LN_NODE_AUTH; this.CORE_RPC = configs.CORE_RPC; diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 37b5301cf..6717154aa 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -213,6 +213,8 @@ class DatabaseMigration { updated_at datetime DEFAULT NULL, created datetime DEFAULT NULL, status int(11) NOT NULL DEFAULT 0, + closing_transaction_id varchar(64) DEFAULT NULL, + closing_reason int(11) DEFAULT NULL, node1_public_key varchar(66) NOT NULL, node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, node1_cltv_delta int(11) DEFAULT NULL, diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 65ecec8c2..3e6bfc96c 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -5,6 +5,10 @@ import lightningApi from '../api/lightning/lightning-api-factory'; import { ILightningApi } from '../api/lightning/lightning-api.interface'; import channelsApi from '../api/explorer/channels.api'; import bitcoinClient from '../api/bitcoin/bitcoin-client'; +import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; +import config from '../config'; +import { IEsploraApi } from '../api/bitcoin/esplora-api.interface'; +import e from 'express'; class NodeSyncService { constructor() {} @@ -38,15 +42,18 @@ class NodeSyncService { await this.$findInactiveNodesAndChannels(); logger.debug(`Inactive channels scan complete`); - await this.$scanForClosedChannels(); - logger.debug(`Closed channels scan complete`); - await this.$lookUpCreationDateFromChain(); logger.debug(`Channel creation dates scan complete`); await this.$updateNodeFirstSeen(); logger.debug(`Node first seen dates scan complete`); + await this.$scanForClosedChannels(); + logger.debug(`Closed channels scan complete`); + + await this.$runClosedChannelsForensics(); + logger.debug(`Closed channels forensics scan complete`); + } catch (e) { logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); } @@ -109,17 +116,129 @@ class NodeSyncService { try { const channels = await channelsApi.$getChannelsByStatus(0); for (const channel of channels) { - const outspends = await bitcoinClient.getTxOut(channel.transaction_id, channel.transaction_vout); - if (outspends === null) { + const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); + if (spendingTx.spent === true && spendingTx.status?.confirmed === true) { logger.debug('Marking channel: ' + channel.id + ' as closed.'); await DB.query(`UPDATE channels SET status = 2 WHERE id = ?`, [channel.id]); + if (spendingTx.txid && !channel.closing_transaction_id) { + await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]); + } } } } catch (e) { - logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); + logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); } } + /* + 1. Mutually closed + 2. Forced closed + 3. Forced closed with penalty + */ + + private async $runClosedChannelsForensics(): Promise { + if (!config.ESPLORA.REST_API_URL) { + return; + } + + try { + const channels = await channelsApi.$getClosedChannelsWithoutReason(); + for (const channel of channels) { + let reason = 0; + // Only Esplora backend can retrieve spent transaction outputs + const outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id); + const lightningScriptReasons: number[] = []; + for (const outspend of outspends) { + if (outspend.spent && outspend.txid) { + const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid); + const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); + lightningScriptReasons.push(lightningScript); + } + } + if (lightningScriptReasons.length === outspends.length + && lightningScriptReasons.filter((r) => r === 1).length === outspends.length) { + reason = 1; + } else { + const filteredReasons = lightningScriptReasons.filter((r) => r !== 1); + if (filteredReasons.length) { + if (filteredReasons.some((r) => r === 2 || r === 4)) { + reason = 3; + } else { + reason = 2; + } + } else { + /* + We can detect a commitment transaction (force close) by reading Sequence and Locktime + https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction + */ + const closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id); + const sequenceHex: string = closingTx.vin[0].sequence.toString(16); + const locktimeHex: string = closingTx.locktime.toString(16); + if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') { + reason = 2; // Here we can't be sure if it's a penalty or not + } else { + reason = 1; + } + } + } + if (reason) { + logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.'); + await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]); + } + } + } catch (e) { + logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private findLightningScript(vin: IEsploraApi.Vin): number { + const topElement = vin.witness[vin.witness.length - 2]; + if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs + if (topElement === '01') { + // top element is '01' to get in the revocation path + // 'Revoked Lightning Force Close'; + // Penalty force closed + return 2; + } else { + // top element is '', this is a delayed to_local output + // 'Lightning Force Close'; + return 3; + } + } else if ( + /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) || + /^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(vin.inner_witnessscript_asm) + ) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs + // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs + if (topElement.length === 66) { + // top element is a public key + // 'Revoked Lightning HTLC'; Penalty force closed + return 4; + } else if (topElement) { + // top element is a preimage + // 'Lightning HTLC'; + return 5; + } else { + // top element is '' to get in the expiry of the script + // 'Expired Lightning HTLC'; + return 6; + } + } else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(vin.inner_witnessscript_asm)) { + // https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors + if (topElement) { + // top element is a signature + // 'Lightning Anchor'; + return 7; + } else { + // top element is '', it has been swept after 16 blocks + // 'Swept Lightning Anchor'; + return 8; + } + } + return 1; + } + private async $saveChannel(channel: ILightningApi.Channel): Promise { const fromChannel = chanNumber({ channel: channel.id }).number; From da9834d2727d9f238276db0703dde5ab43d9e4ef Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 30 Jun 2022 00:35:27 +0200 Subject: [PATCH 25/51] Label channel closes --- .../address-labels.component.ts | 3 ++- .../transactions-list.component.html | 4 ++-- .../transactions-list.component.ts | 2 +- frontend/src/app/services/api.service.ts | 4 ++-- .../src/api/explorer/channels.api.ts | 2 +- .../src/api/explorer/channels.routes.ts | 22 ++++++++++++++----- lightning-backend/src/database-migration.ts | 3 ++- 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index f2018bfe5..ba03aada8 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -34,7 +34,8 @@ export class AddressLabelsComponent implements OnChanges { } handleChannel() { - this.label = `Channel open: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; + const type = this.vout ? 'open' : 'close'; + this.label = `Channel ${type}: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; } handleVin() { diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 9119355f6..3e78f60a2 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -77,7 +77,7 @@ {{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
- +
@@ -172,7 +172,7 @@
- +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 0ea41a2bb..e1e9880c0 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -36,7 +36,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { showDetails$ = new BehaviorSubject(false); outspends: Outspend[][] = []; assetsMinimal: any; - channels: any[]; + channels: { inputs: any[], outputs: any[] }; constructor( public stateService: StateService, diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 1c439a755..f728e64d4 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -232,12 +232,12 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); } - getChannelByTxIds$(txIds: string[]): Observable { + getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> { let params = new HttpParams(); txIds.forEach((txId: string) => { params = params.append('txId[]', txId); }); - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params }); + return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params }); } lightningSearch$(searchText: string): Observable { diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/lightning-backend/src/api/explorer/channels.api.ts index d2dd930c3..c72f93257 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/lightning-backend/src/api/explorer/channels.api.ts @@ -74,7 +74,7 @@ class ChannelsApi { public async $getChannelsByTransactionId(transactionIds: string[]): Promise { try { transactionIds = transactionIds.map((id) => '\'' + id + '\''); - 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(', ')})`; + 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(', ')})`; const [rows]: any = await DB.query(query); const channels = rows.map((row) => this.convertChannel(row)); return channels; diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/lightning-backend/src/api/explorer/channels.routes.ts index fd3b3fac0..ff9d166f9 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/lightning-backend/src/api/explorer/channels.routes.ts @@ -67,17 +67,27 @@ class ChannelsRoutes { } } const channels = await channelsApi.$getChannelsByTransactionId(txIds); - const result: any[] = []; + const inputs: any[] = []; + const outputs: any[] = []; for (const txid of txIds) { - const foundChannel = channels.find((channel) => channel.transaction_id === txid); - if (foundChannel) { - result.push(foundChannel); + const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid); + if (foundChannelInputs) { + inputs.push(foundChannelInputs); } else { - result.push(null); + inputs.push(null); + } + const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid); + if (foundChannelOutputs) { + outputs.push(foundChannelOutputs); + } else { + outputs.push(null); } } - res.json(result); + res.json({ + inputs: inputs, + outputs: outputs, + }); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 6717154aa..4b99a708f 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -236,7 +236,8 @@ class DatabaseMigration { KEY node2_public_key (node2_public_key), KEY status (status), KEY short_id (short_id), - KEY transaction_id (transaction_id) + KEY transaction_id (transaction_id), + KEY closing_transaction_id (closing_transaction_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } From 4d83478e7d9bcf22c435e6bf558e2e9dd3f9b6f8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 1 Jul 2022 14:58:17 +0200 Subject: [PATCH 26/51] Adding TLV to channel details --- .../channel/channel-box/channel-box.component.html | 10 +++++++++- .../channel/channel-box/channel-box.component.scss | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 04471d351..51daf4f05 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -40,7 +40,15 @@ + + Timelock delta + + + +
-
\ No newline at end of file +
+ +{{ i }} blocks \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss index 1041633f6..300b98b11 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.scss @@ -11,3 +11,8 @@ width: 50%; margin-top: auto; } + +.shared-block { + color: #ffffff66; + font-size: 12px; +} From 3152effba51a0c3a27a8bc7af60e28e4fec05a05 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 1 Jul 2022 15:24:14 +0200 Subject: [PATCH 27/51] Sats component rounding --- .../lightning/channel/channel-box/channel-box.component.html | 2 +- frontend/src/app/shared/components/sats/sats.component.html | 2 +- frontend/src/app/shared/components/sats/sats.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html index 51daf4f05..382fffd47 100644 --- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html +++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html @@ -25,7 +25,7 @@ Base fee - + diff --git a/frontend/src/app/shared/components/sats/sats.component.html b/frontend/src/app/shared/components/sats/sats.component.html index d8b52b956..a648cdfcb 100644 --- a/frontend/src/app/shared/components/sats/sats.component.html +++ b/frontend/src/app/shared/components/sats/sats.component.html @@ -1,4 +1,4 @@ -‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : '1.0-0' }} +‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} L- tL- t- diff --git a/frontend/src/app/shared/components/sats/sats.component.ts b/frontend/src/app/shared/components/sats/sats.component.ts index f341b3027..d9801d249 100644 --- a/frontend/src/app/shared/components/sats/sats.component.ts +++ b/frontend/src/app/shared/components/sats/sats.component.ts @@ -9,7 +9,7 @@ import { StateService } from '../../../services/state.service'; }) export class SatsComponent implements OnInit { @Input() satoshis: number; - @Input() digitsInfo = 0; + @Input() digitsInfo = '1.0-0'; @Input() addPlus = false; network = ''; From 24631116c49e96d1ac6aec58860288fe8aee90a3 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 1 Jul 2022 16:50:53 +0200 Subject: [PATCH 28/51] Dashboard stats graph --- .../channels-list/channels-list.component.ts | 2 + .../app/lightning/lightning-api.service.ts | 4 + .../lightning-dashboard.component.html | 14 + .../src/app/lightning/lightning.module.ts | 6 + .../app/lightning/node/node.component.html | 14 - .../lightning-statistics-chart.component.html | 32 ++ .../lightning-statistics-chart.component.scss | 135 ++++++++ .../lightning-statistics-chart.component.ts | 301 ++++++++++++++++++ .../src/api/explorer/general.routes.ts | 11 + .../src/api/explorer/statistics.api.ts | 17 + 10 files changed, 522 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html create mode 100644 frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss create mode 100644 frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts create mode 100644 lightning-backend/src/api/explorer/statistics.api.ts diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index debf2467a..0ac7da578 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -40,6 +40,8 @@ export class ChannelsListComponent implements OnInit, OnChanges { } ngOnChanges(): void { + this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false }) + this.channels$ = combineLatest([ this.channelsPage$, this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus)) diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 9197f4f02..5a6e63305 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -41,4 +41,8 @@ export class LightningApiService { listTopNodes$(): Observable { return this.httpClient.get(API_BASE_URL + '/nodes/top'); } + + listStatistics$(): Observable { + return this.httpClient.get(API_BASE_URL + '/statistics'); + } } diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 31b0784d6..23c2c80ae 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -24,6 +24,20 @@
+
+
+
+ +
+
+
+ +
+
+ +
+
+
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index a8cad3dc9..48fc1c696 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -13,6 +13,8 @@ import { ChannelComponent } from './channel/channel.component'; import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; +import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; +import { NgxEchartsModule } from 'ngx-echarts'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -24,12 +26,16 @@ import { ClosingTypeComponent } from './channel/closing-type/closing-type.compon LightningWrapperComponent, ChannelBoxComponent, ClosingTypeComponent, + LightningStatisticsChartComponent, ], imports: [ CommonModule, SharedModule, RouterModule, LightningRoutingModule, + NgxEchartsModule.forRoot({ + echarts: () => import('echarts') + }) ], providers: [ LightningApiService, diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 93bb67e61..19f730d3e 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -91,20 +91,6 @@ - -

diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html new file mode 100644 index 000000000..252947352 --- /dev/null +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html @@ -0,0 +1,32 @@ +
+ +
+ Hashrate & Difficulty + +
+ +
+
+
+
+ +
+ + +
+
+
Hashrate
+

+ +

+
+
+
Difficulty
+

+ +

+
+
+
diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss new file mode 100644 index 000000000..fa044a4d6 --- /dev/null +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss @@ -0,0 +1,135 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + padding: 0px 15px; + width: 100%; + min-height: 500px; + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; + }; +} + +.chart { + width: 100%; + height: 100%; + padding-bottom: 20px; + padding-right: 10px; + @media (max-width: 992px) { + padding-bottom: 25px; + } + @media (max-width: 829px) { + padding-bottom: 50px; + } + @media (max-width: 767px) { + padding-bottom: 25px; + } + @media (max-width: 629px) { + padding-bottom: 55px; + } + @media (max-width: 567px) { + padding-bottom: 55px; + } +} +.chart-widget { + width: 100%; + height: 100%; + max-height: 270px; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 991px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 991px) { + position: relative; + top: 0px; + } + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts new file mode 100644 index 000000000..3d5a81afd --- /dev/null +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -0,0 +1,301 @@ +import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable } from 'rxjs'; +import { startWith, switchMap, tap } from 'rxjs/operators'; +import { SeoService } from 'src/app/services/seo.service'; +import { formatNumber } from '@angular/common'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; +import { download } from 'src/app/shared/graphs.utils'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-lightning-statistics-chart', + templateUrl: './lightning-statistics-chart.component.html', + styleUrls: ['./lightning-statistics-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class LightningStatisticsChartComponent implements OnInit { + @Input() right: number | string = 65; + @Input() left: number | string = 55; + @Input() widget = false; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + @HostBinding('attr.dir') dir = 'ltr'; + + blockSizesWeightsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + chartInstance: any = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private lightningApiService: LightningApiService, + private formBuilder: FormBuilder, + private storageService: StorageService, + private miningService: MiningService, + ) { + } + + ngOnInit(): void { + let firstRun = true; + + this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Weight`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); + + this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith(this.miningWindowPreference), + switchMap((timespan) => { + this.timespan = timespan; + if (!firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; + this.miningWindowPreference = timespan; + this.isLoading = true; + return this.lightningApiService.listStatistics$() + .pipe( + tap((data) => { + this.prepareChartOptions({ + nodes: data.map(val => [val.added * 1000, val.node_count]), + capacity: data.map(val => [val.added * 1000, val.total_capacity]), + }); + this.isLoading = false; + }), + ); + }), + ).subscribe(() => { + }); + } + + prepareChartOptions(data) { + let title: object; + if (data.nodes.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: `Indexing in progess`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + '#FDD835', + '#D81B60', + ], + grid: { + top: 30, + bottom: 70, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: (ticks) => { + let sizeString = ''; + let weightString = ''; + + for (const tick of ticks) { + if (tick.seriesIndex === 0) { // Nodes + sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')}`; + } else if (tick.seriesIndex === 1) { // Capacity + weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100000000, this.locale, '1.0-0')} BTC`; + } + } + + const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); + + let tooltip = `${date}
+ ${sizeString}
+ ${weightString}`; + + return tooltip; + } + }, + xAxis: data.nodes.length === 0 ? undefined : { + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + axisLabel: { + hideOverlap: true, + } + }, + legend: data.nodes.length === 0 ? undefined : { + padding: 10, + data: [ + { + name: 'Nodes', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Capacity', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], + selected: JSON.parse(this.storageService.getValue('sizes_ln_legend')) ?? { + 'Nodes': true, + 'Capacity': true, + } + }, + yAxis: data.nodes.length === 0 ? undefined : [ + { + min: (value) => { + return value.min * 0.9; + }, + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${Math.round(val)}`; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + }, + { + min: (value) => { + return value.min * 0.9; + }, + type: 'value', + position: 'right', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val / 100000000} BTC`; + } + }, + splitLine: { + show: false, + } + } + ], + series: data.nodes.length === 0 ? [] : [ + { + zlevel: 0, + name: 'Nodes', + showSymbol: false, + symbol: 'none', + data: data.nodes, + type: 'line', + lineStyle: { + width: 2, + }, + markLine: { + silent: true, + symbol: 'none', + lineStyle: { + type: 'solid', + color: '#ffffff66', + opacity: 1, + width: 1, + }, + data: [{ + yAxis: 1, + label: { + position: 'end', + show: true, + color: '#ffffff', + formatter: `1 MB` + } + }], + } + }, + { + zlevel: 1, + yAxisIndex: 1, + name: 'Capacity', + showSymbol: false, + symbol: 'none', + data: data.capacity, + type: 'line', + lineStyle: { + width: 2, + } + } + ], + }; + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + + this.chartInstance.on('legendselectchanged', (e) => { + this.storageService.setValue('sizes_ln_legend', JSON.stringify(e.selected)); + }); + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + const now = new Date(); + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = '#11131f'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + }), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); + // @ts-ignore + this.chartOptions.grid.bottom = prevBottom; + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } +} diff --git a/lightning-backend/src/api/explorer/general.routes.ts b/lightning-backend/src/api/explorer/general.routes.ts index 326602d55..d6154bdc1 100644 --- a/lightning-backend/src/api/explorer/general.routes.ts +++ b/lightning-backend/src/api/explorer/general.routes.ts @@ -2,12 +2,14 @@ import config from '../../config'; import { Express, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; +import statisticsApi from './statistics.api'; class GeneralRoutes { constructor() { } public initRoutes(app: Express) { app .get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'statistics', this.$getStatistics) ; } @@ -27,6 +29,15 @@ class GeneralRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getStatistics(req: Request, res: Response) { + try { + const statistics = await statisticsApi.$getStatistics(); + res.json(statistics); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new GeneralRoutes(); diff --git a/lightning-backend/src/api/explorer/statistics.api.ts b/lightning-backend/src/api/explorer/statistics.api.ts new file mode 100644 index 000000000..620e76fef --- /dev/null +++ b/lightning-backend/src/api/explorer/statistics.api.ts @@ -0,0 +1,17 @@ +import logger from '../../logger'; +import DB from '../../database'; + +class StatisticsApi { + public async $getStatistics(): Promise { + try { + const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM statistics ORDER BY id DESC`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } +} + +export default new StatisticsApi(); From d8a39f2e4947b2252be62d8631f9c2c49ed5ca19 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 1 Jul 2022 18:06:48 +0200 Subject: [PATCH 29/51] Timestamp component --- .../app/components/block/block.component.html | 5 +--- .../lightning/channel/channel.component.html | 4 +-- .../app/lightning/node/node.component.html | 4 +-- .../timestamp/timestamp.component.html | 4 +++ .../timestamp/timestamp.component.scss | 0 .../timestamp/timestamp.component.ts | 25 +++++++++++++++++++ frontend/src/app/shared/shared.module.ts | 3 +++ 7 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 frontend/src/app/shared/components/timestamp/timestamp.component.html create mode 100644 frontend/src/app/shared/components/timestamp/timestamp.component.scss create mode 100644 frontend/src/app/shared/components/timestamp/timestamp.component.ts diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 2a3d8f2fd..d21cea34e 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -55,10 +55,7 @@ Timestamp - ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} -
- () -
+ diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 43dafd24e..41c2f3254 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -22,11 +22,11 @@ Created - + Last update - + Opening transaction diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 19f730d3e..101250359 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -43,13 +43,13 @@ First seen - + Last update - + diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.html b/frontend/src/app/shared/components/timestamp/timestamp.component.html new file mode 100644 index 000000000..b37ff065a --- /dev/null +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.html @@ -0,0 +1,4 @@ +‎{{ seconds * 1000 | date:'yyyy-MM-dd HH:mm' }} +
+ () +
diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.scss b/frontend/src/app/shared/components/timestamp/timestamp.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/timestamp/timestamp.component.ts b/frontend/src/app/shared/components/timestamp/timestamp.component.ts new file mode 100644 index 000000000..a0c9861f0 --- /dev/null +++ b/frontend/src/app/shared/components/timestamp/timestamp.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-timestamp', + templateUrl: './timestamp.component.html', + styleUrls: ['./timestamp.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TimestampComponent implements OnChanges { + @Input() unixTime: number; + @Input() dateString: string; + + seconds: number; + + constructor() { } + + ngOnChanges(): void { + if (this.unixTime) { + this.seconds = this.unixTime; + } else if (this.dateString) { + this.seconds = new Date(this.dateString).getTime() / 1000 + } + } + +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index bbcba71c0..77e4cb046 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -76,6 +76,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen import { ChangeComponent } from '../components/change/change.component'; import { SatsComponent } from './components/sats/sats.component'; import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; +import { TimestampComponent } from './components/timestamp/timestamp.component'; @NgModule({ declarations: [ @@ -146,6 +147,7 @@ import { SearchResultsComponent } from '../components/search-form/search-results ChangeComponent, SatsComponent, SearchResultsComponent, + TimestampComponent, ], imports: [ CommonModule, @@ -244,6 +246,7 @@ import { SearchResultsComponent } from '../components/search-form/search-results ChangeComponent, SatsComponent, SearchResultsComponent, + TimestampComponent, ] }) export class SharedModule { From f46543b264ecc66e954f51ac143ca5ea429a9ec8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 2 Jul 2022 16:46:57 +0200 Subject: [PATCH 30/51] Node graphs --- .../src/app/lightning/lightning.module.ts | 2 + .../node-statistics-chart.component.html | 8 + .../node-statistics-chart.component.scss | 129 ++++++++ .../node-statistics-chart.component.ts | 287 ++++++++++++++++++ .../app/lightning/node/node.component.html | 4 + .../src/app/lightning/node/node.component.ts | 7 - .../lightning-statistics-chart.component.ts | 11 +- .../src/api/explorer/nodes.api.ts | 2 +- 8 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 48fc1c696..284c252bc 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -14,12 +14,14 @@ import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; +import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; import { NgxEchartsModule } from 'ngx-echarts'; @NgModule({ declarations: [ LightningDashboardComponent, NodesListComponent, NodeStatisticsComponent, + NodeStatisticsChartComponent, NodeComponent, ChannelsListComponent, ChannelComponent, diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html new file mode 100644 index 000000000..c5cad52fa --- /dev/null +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html @@ -0,0 +1,8 @@ +
+ +
+
+
+
+ +
diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss new file mode 100644 index 000000000..85e7c5e68 --- /dev/null +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss @@ -0,0 +1,129 @@ + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + padding: 0px 15px; + width: 100%; + /* min-height: 500px; */ + height: calc(100% - 150px); + @media (max-width: 992px) { + height: 100%; + padding-bottom: 100px; + }; +} +/* +.chart { + width: 100%; + height: 100%; + padding-bottom: 20px; + padding-right: 10px; + @media (max-width: 992px) { + padding-bottom: 25px; + } + @media (max-width: 829px) { + padding-bottom: 50px; + } + @media (max-width: 767px) { + padding-bottom: 25px; + } + @media (max-width: 629px) { + padding-bottom: 55px; + } + @media (max-width: 567px) { + padding-bottom: 55px; + } +} +*/ +.chart-widget { + width: 100%; + height: 100%; + max-height: 270px; +} + +.formRadioGroup { + margin-top: 6px; + display: flex; + flex-direction: column; + @media (min-width: 991px) { + position: relative; + top: -65px; + } + @media (min-width: 830px) and (max-width: 991px) { + position: relative; + top: 0px; + } + @media (min-width: 830px) { + flex-direction: row; + float: right; + margin-top: 0px; + } + .btn-sm { + font-size: 9px; + @media (min-width: 830px) { + font-size: 14px; + } + } +} + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } +} + +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts new file mode 100644 index 000000000..15997d3c3 --- /dev/null +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts @@ -0,0 +1,287 @@ +import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; +import { EChartsOption } from 'echarts'; +import { Observable } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; +import { formatNumber } from '@angular/common'; +import { FormGroup } from '@angular/forms'; +import { StorageService } from 'src/app/services/storage.service'; +import { download } from 'src/app/shared/graphs.utils'; +import { LightningApiService } from '../lightning-api.service'; +import { ActivatedRoute, ParamMap } from '@angular/router'; + +@Component({ + selector: 'app-node-statistics-chart', + templateUrl: './node-statistics-chart.component.html', + styleUrls: ['./node-statistics-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class NodeStatisticsChartComponent implements OnInit { + @Input() publicKey: string; + @Input() right: number | string = 65; + @Input() left: number | string = 55; + @Input() widget = false; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + @HostBinding('attr.dir') dir = 'ltr'; + + blockSizesWeightsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + chartInstance: any = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private lightningApiService: LightningApiService, + private storageService: StorageService, + private activatedRoute: ActivatedRoute, + ) { + } + + ngOnInit(): void { + + this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + this.isLoading = true; + return this.lightningApiService.listNodeStats$(params.get('public_key')) + .pipe( + tap((data) => { + this.prepareChartOptions({ + channels: data.map(val => [val.added * 1000, val.channels]), + capacity: data.map(val => [val.added * 1000, val.capacity]), + }); + this.isLoading = false; + }), + ); + }), + ).subscribe(() => { + }); + } + + prepareChartOptions(data) { + let title: object; + if (data.channels.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: `Loading`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + '#FDD835', + '#D81B60', + ], + grid: { + top: 30, + bottom: 70, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: (ticks) => { + let sizeString = ''; + let weightString = ''; + + for (const tick of ticks) { + if (tick.seriesIndex === 0) { // Channels + sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 1) { // Capacity + weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100000000, this.locale, '1.0-0')} BTC`; + } + } + + const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); + + const tooltip = `${date}
+ ${sizeString}
+ ${weightString}`; + + return tooltip; + } + }, + xAxis: data.channels.length === 0 ? undefined : { + type: 'time', + splitNumber: this.isMobile() ? 5 : 10, + axisLabel: { + hideOverlap: true, + } + }, + legend: data.channels.length === 0 ? undefined : { + padding: 10, + data: [ + { + name: 'Channels', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Capacity', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], + selected: JSON.parse(this.storageService.getValue('sizes_ln_legend')) ?? { + 'Channels': true, + 'Capacity': true, + } + }, + yAxis: data.channels.length === 0 ? undefined : [ + { + min: (value) => { + return value.min * 0.9; + }, + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${Math.round(val)}`; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + }, + { + min: (value) => { + return value.min * 0.9; + }, + type: 'value', + position: 'right', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${val / 100000000} BTC`; + } + }, + splitLine: { + show: false, + } + } + ], + series: data.channels.length === 0 ? [] : [ + { + zlevel: 1, + name: 'Channels', + showSymbol: false, + symbol: 'none', + data: data.channels, + type: 'line', + step: 'middle', + lineStyle: { + width: 2, + }, + markLine: { + silent: true, + symbol: 'none', + lineStyle: { + type: 'solid', + color: '#ffffff66', + opacity: 1, + width: 1, + }, + data: [{ + yAxis: 1, + label: { + position: 'end', + show: true, + color: '#ffffff', + formatter: `1 MB` + } + }], + } + }, + { + zlevel: 0, + yAxisIndex: 1, + name: 'Capacity', + showSymbol: false, + symbol: 'none', + stack: 'Total', + data: data.capacity, + areaStyle: {}, + type: 'line', + step: 'middle', + } + ], + }; + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + + this.chartInstance.on('legendselectchanged', (e) => { + this.storageService.setValue('sizes_ln_legend', JSON.stringify(e.selected)); + }); + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + const now = new Date(); + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = '#11131f'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + }), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); + // @ts-ignore + this.chartOptions.grid.bottom = prevBottom; + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } +} diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 101250359..cf09c8868 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -86,6 +86,10 @@
+ +
+ +
diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 3f2af52b9..1c6c5ee23 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -55,13 +55,6 @@ export class NodeComponent implements OnInit { return node; }), ); - - this.statistics$ = this.activatedRoute.paramMap - .pipe( - switchMap((params: ParamMap) => { - return this.lightningApiService.listNodeStats$(params.get('public_key')); - }) - ); } changeSocket(index: number) { diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 3d5a81afd..0d7599ac4 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -135,7 +135,7 @@ export class LightningStatisticsChartComponent implements OnInit { for (const tick of ticks) { if (tick.seriesIndex === 0) { // Nodes - sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')}`; + sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 1) { // Capacity weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100000000, this.locale, '1.0-0')} BTC`; } @@ -221,7 +221,7 @@ export class LightningStatisticsChartComponent implements OnInit { ], series: data.nodes.length === 0 ? [] : [ { - zlevel: 0, + zlevel: 1, name: 'Nodes', showSymbol: false, symbol: 'none', @@ -251,16 +251,15 @@ export class LightningStatisticsChartComponent implements OnInit { } }, { - zlevel: 1, + zlevel: 0, yAxisIndex: 1, name: 'Capacity', showSymbol: false, symbol: 'none', + stack: 'Total', data: data.capacity, + areaStyle: {}, type: 'line', - lineStyle: { - width: 2, - } } ], }; diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts index 391056d0b..4c80c7f2d 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/lightning-backend/src/api/explorer/nodes.api.ts @@ -15,7 +15,7 @@ class NodesApi { public async $getNodeStats(public_key: string): Promise { try { - const query = `SELECT * FROM node_stats WHERE public_key = ? ORDER BY added DESC`; + const query = `SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels FROM node_stats WHERE public_key = ? ORDER BY added DESC`; const [rows]: any = await DB.query(query, [public_key]); return rows; } catch (e) { From f2e42b17a75c4932eccc363d6c1376c13328bd89 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 16:39:06 +0200 Subject: [PATCH 31/51] Fix TLS misspelling --- lightning-backend/mempool-config.sample.json | 2 +- lightning-backend/src/api/lightning/lnd/lnd-api.ts | 2 +- lightning-backend/src/config.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index 9402ff2c2..30f749040 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -17,7 +17,7 @@ "FACILITY": "local7" }, "LN_NODE_AUTH": { - "TSL_CERT_PATH": "", + "TLS_CERT_PATH": "", "MACAROON_PATH": "" }, "CORE_RPC": { diff --git a/lightning-backend/src/api/lightning/lnd/lnd-api.ts b/lightning-backend/src/api/lightning/lnd/lnd-api.ts index 3388a04b3..562d3f663 100644 --- a/lightning-backend/src/api/lightning/lnd/lnd-api.ts +++ b/lightning-backend/src/api/lightning/lnd/lnd-api.ts @@ -9,7 +9,7 @@ class LndApi implements AbstractLightningApi { private lnd: any; constructor() { try { - const tsl = fs.readFileSync(config.LN_NODE_AUTH.TSL_CERT_PATH).toString('base64'); + const tsl = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64'); const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64'); const { lnd } = lnService.authenticatedLndGrpc({ diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index d2edad180..25893beba 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -19,7 +19,7 @@ interface IConfig { FACILITY: string; }; LN_NODE_AUTH: { - TSL_CERT_PATH: string; + TLS_CERT_PATH: string; MACAROON_PATH: string; }; CORE_RPC: { @@ -57,7 +57,7 @@ const defaults: IConfig = { 'FACILITY': 'local7' }, 'LN_NODE_AUTH': { - 'TSL_CERT_PATH': '', + 'TLS_CERT_PATH': '', 'MACAROON_PATH': '', }, 'CORE_RPC': { From a8fd04e2f0b248860579a336269838caa7bfb340 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 20:03:58 +0200 Subject: [PATCH 32/51] Adding missing migration index key --- lightning-backend/src/database-migration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index 4b99a708f..bedc2f856 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -199,7 +199,8 @@ class DatabaseMigration { alias varchar(200) CHARACTER SET utf8mb4 NOT NULL, color varchar(200) NOT NULL, sockets text DEFAULT NULL, - PRIMARY KEY (public_key) + PRIMARY KEY (public_key), + KEY alias (alias(10)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } From 3ecce35b11733d766908a8d950924d8d00ef9853 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 20:07:54 +0200 Subject: [PATCH 33/51] Minor tls spelling fix --- lightning-backend/src/api/lightning/lnd/lnd-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning-backend/src/api/lightning/lnd/lnd-api.ts b/lightning-backend/src/api/lightning/lnd/lnd-api.ts index 562d3f663..96b12f0f7 100644 --- a/lightning-backend/src/api/lightning/lnd/lnd-api.ts +++ b/lightning-backend/src/api/lightning/lnd/lnd-api.ts @@ -9,11 +9,11 @@ class LndApi implements AbstractLightningApi { private lnd: any; constructor() { try { - const tsl = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64'); + const tls = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64'); const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64'); const { lnd } = lnService.authenticatedLndGrpc({ - cert: tsl, + cert: tls, macaroon: macaroon, socket: 'localhost:10009', }); From cea7ce140f79c2136b4a861bfa10945683dfe29f Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 20:10:21 +0200 Subject: [PATCH 34/51] Making socket configurable --- lightning-backend/mempool-config.sample.json | 3 ++- lightning-backend/src/api/lightning/lnd/lnd-api.ts | 2 +- lightning-backend/src/config.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index 30f749040..2a2d3d6a9 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -18,7 +18,8 @@ }, "LN_NODE_AUTH": { "TLS_CERT_PATH": "", - "MACAROON_PATH": "" + "MACAROON_PATH": "", + "SOCKET": "localhost:10009" }, "CORE_RPC": { "HOST": "127.0.0.1", diff --git a/lightning-backend/src/api/lightning/lnd/lnd-api.ts b/lightning-backend/src/api/lightning/lnd/lnd-api.ts index 96b12f0f7..57f225101 100644 --- a/lightning-backend/src/api/lightning/lnd/lnd-api.ts +++ b/lightning-backend/src/api/lightning/lnd/lnd-api.ts @@ -15,7 +15,7 @@ class LndApi implements AbstractLightningApi { const { lnd } = lnService.authenticatedLndGrpc({ cert: tls, macaroon: macaroon, - socket: 'localhost:10009', + socket: config.LN_NODE_AUTH.SOCKET, }); this.lnd = lnd; diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index 25893beba..df821f6fb 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -21,6 +21,7 @@ interface IConfig { LN_NODE_AUTH: { TLS_CERT_PATH: string; MACAROON_PATH: string; + SOCKET: string; }; CORE_RPC: { HOST: string; @@ -59,6 +60,7 @@ const defaults: IConfig = { 'LN_NODE_AUTH': { 'TLS_CERT_PATH': '', 'MACAROON_PATH': '', + 'SOCKET': 'localhost:10009', }, 'CORE_RPC': { 'HOST': '127.0.0.1', From d32579dfb5557261179405297ef60d9feb3f4552 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 20:22:40 +0200 Subject: [PATCH 35/51] Adding missing *.json files --- lightning-backend/.gitignore | 4 + lightning-backend/package-lock.json | 3291 +++++++++++++++++++++++++++ lightning-backend/package.json | 25 + lightning-backend/tsconfig.json | 22 + lightning-backend/tslint.json | 137 ++ 5 files changed, 3479 insertions(+) create mode 100644 lightning-backend/package-lock.json create mode 100644 lightning-backend/package.json create mode 100644 lightning-backend/tsconfig.json create mode 100644 lightning-backend/tslint.json diff --git a/lightning-backend/.gitignore b/lightning-backend/.gitignore index 4aa51e0c1..3a41effef 100644 --- a/lightning-backend/.gitignore +++ b/lightning-backend/.gitignore @@ -3,6 +3,10 @@ # production config and external assets *.json !mempool-config.sample.json +!package.json +!package-lock.json +!tslint.json +!tsconfig.json # compiled output /dist diff --git a/lightning-backend/package-lock.json b/lightning-backend/package-lock.json new file mode 100644 index 000000000..38a1a4571 --- /dev/null +++ b/lightning-backend/package-lock.json @@ -0,0 +1,3291 @@ +{ + "name": "lightning-backend", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "lightning-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^0.27.2", + "express": "^4.17.3", + "ln-service": "^53.11.0", + "mysql2": "^2.3.3", + "typescript": "^4.6.3" + }, + "devDependencies": { + "@types/express": "^4.17.13", + "@types/node": "^17.0.24" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/node": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, + "node_modules/asyncjs-util": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", + "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", + "dependencies": { + "async": "3.2.3" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/bip174": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", + "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" + }, + "node_modules/bitcoinjs-lib": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", + "integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==", + "dependencies": { + "bech32": "^2.0.0", + "bip174": "^2.0.1", + "bs58check": "^2.1.2", + "create-hash": "^1.1.0", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2", + "wif": "^2.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, + "node_modules/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/bolt07": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", + "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", + "dependencies": { + "bn.js": "5.2.0" + } + }, + "node_modules/bolt09": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.2.tgz", + "integrity": "sha512-m533YWZ/R/p1buxEK/19v94Ay1vS1PJNwfP30BCVj6l96NGpOa9t40HYuMpoX+xFYwOx8kZs+GGTb9TbJund0w==", + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ecpair": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", + "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", + "dependencies": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/invoices": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.5.tgz", + "integrity": "sha512-097isfZK3qaDJXQOEqTr3IfnrFZnGCAsbyqWNHAESdG12vBC39dprZWFwPLtnv7I8exhJG6WFFlaC51qaJan/w==", + "dependencies": { + "bech32": "2.0.0", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "bolt07": "1.8.1", + "bolt09": "0.2.2", + "tiny-secp256k1": "2.2.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "node_modules/lightning": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.10.1.tgz", + "integrity": "sha512-dIrN4vPzmzq9DaMD6c+9DqQwJCMl1lOleWrhIrv+HIpzq6rdNJvUXaVJOFz1OV8P3zy2Q3+s9VxnzeN70ee+ow==", + "dependencies": { + "@grpc/grpc-js": "1.6.2", + "@grpc/proto-loader": "0.6.9", + "@types/express": "4.17.13", + "@types/node": "17.0.23", + "@types/request": "2.48.8", + "@types/ws": "8.5.3", + "async": "3.2.3", + "asyncjs-util": "1.2.9", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "body-parser": "1.20.0", + "bolt07": "1.8.1", + "bolt09": "0.2.3", + "cbor": "8.1.0", + "ecpair": "2.0.1", + "express": "4.17.3", + "invoices": "2.0.5", + "psbt": "2.0.1", + "tiny-secp256k1": "2.2.1", + "type-fest": "2.12.2" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/lightning/node_modules/@grpc/grpc-js": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", + "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "dependencies": { + "@grpc/proto-loader": "^0.6.4", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/lightning/node_modules/@grpc/proto-loader": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", + "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightning/node_modules/@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "node_modules/lightning/node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/lightning/node_modules/bolt09": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", + "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lightning/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lightning/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/lightning/node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/lightning/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/lightning/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/lightning/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/lightning/node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/lightning/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/lightning/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ln-service": { + "version": "53.11.0", + "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-53.11.0.tgz", + "integrity": "sha512-qdsgLRFGdn8+zfSDgbGw584fS2QQromxp4VRXzj9nk3qveTD6IwBjaEhC1xtY73MQCHQ3ALkWVn3aYMoy5erFw==", + "dependencies": { + "bolt07": "1.8.1", + "cors": "2.8.5", + "express": "4.17.3", + "invoices": "2.0.5", + "lightning": "5.10.1", + "macaroon": "3.0.4", + "morgan": "1.10.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/macaroon": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/macaroon/-/macaroon-3.0.4.tgz", + "integrity": "sha512-Tja2jvupseKxltPZbu5RPSz2Pgh6peYA3O46YCTcYL8PI1VqtGwDqRhGfP8pows26xx9wTiygk+en62Bq+Y8JA==", + "dependencies": { + "sjcl": "^1.0.6", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/mysql2": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", + "dependencies": { + "denque": "^2.0.1", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", + "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", + "dependencies": { + "lru-cache": "^4.1.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/named-placeholders/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "engines": { + "node": ">=12.19" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psbt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.0.1.tgz", + "integrity": "sha512-4s5DSXDJ9xAYjuRJXf8rEuqs+Leyl11TE3y98xzlhMJN2UYXSLkAW1KSUdm/gdu1cSTcdcicIFZscNXmxFko+w==", + "dependencies": { + "bip66": "1.1.5", + "bitcoin-ops": "1.4.1", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "pushdata-bitcoin": "1.0.1", + "varuint-bitcoin": "1.1.2" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "node_modules/pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "dependencies": { + "bitcoin-ops": "^1.3.0" + } + }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" + }, + "node_modules/serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sjcl": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", + "integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tiny-secp256k1": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", + "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, + "node_modules/type-fest": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.2.tgz", + "integrity": "sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "node_modules/typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "dependencies": { + "bs58check": "<3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + } + }, + "dependencies": { + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/node": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/tough-cookie": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + }, + "asyncjs-util": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", + "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", + "requires": { + "async": "3.2.3" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "bip174": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", + "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" + }, + "bitcoinjs-lib": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", + "integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==", + "requires": { + "bech32": "^2.0.0", + "bip174": "^2.0.1", + "bs58check": "^2.1.2", + "create-hash": "^1.1.0", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2", + "wif": "^2.0.1" + } + }, + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, + "body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "bolt07": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", + "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", + "requires": { + "bn.js": "5.2.0" + } + }, + "bolt09": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.2.tgz", + "integrity": "sha512-m533YWZ/R/p1buxEK/19v94Ay1vS1PJNwfP30BCVj6l96NGpOa9t40HYuMpoX+xFYwOx8kZs+GGTb9TbJund0w==" + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "requires": { + "nofilter": "^3.1.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ecpair": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", + "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", + "requires": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "requires": { + "is-property": "^1.0.2" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "invoices": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.5.tgz", + "integrity": "sha512-097isfZK3qaDJXQOEqTr3IfnrFZnGCAsbyqWNHAESdG12vBC39dprZWFwPLtnv7I8exhJG6WFFlaC51qaJan/w==", + "requires": { + "bech32": "2.0.0", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "bolt07": "1.8.1", + "bolt09": "0.2.2", + "tiny-secp256k1": "2.2.1" + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "lightning": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.10.1.tgz", + "integrity": "sha512-dIrN4vPzmzq9DaMD6c+9DqQwJCMl1lOleWrhIrv+HIpzq6rdNJvUXaVJOFz1OV8P3zy2Q3+s9VxnzeN70ee+ow==", + "requires": { + "@grpc/grpc-js": "1.6.2", + "@grpc/proto-loader": "0.6.9", + "@types/express": "4.17.13", + "@types/node": "17.0.23", + "@types/request": "2.48.8", + "@types/ws": "8.5.3", + "async": "3.2.3", + "asyncjs-util": "1.2.9", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "body-parser": "1.20.0", + "bolt07": "1.8.1", + "bolt09": "0.2.3", + "cbor": "8.1.0", + "ecpair": "2.0.1", + "express": "4.17.3", + "invoices": "2.0.5", + "psbt": "2.0.1", + "tiny-secp256k1": "2.2.1", + "type-fest": "2.12.2" + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", + "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "requires": { + "@grpc/proto-loader": "^0.6.4", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", + "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.2.0" + } + }, + "@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bolt09": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", + "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "ln-service": { + "version": "53.11.0", + "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-53.11.0.tgz", + "integrity": "sha512-qdsgLRFGdn8+zfSDgbGw584fS2QQromxp4VRXzj9nk3qveTD6IwBjaEhC1xtY73MQCHQ3ALkWVn3aYMoy5erFw==", + "requires": { + "bolt07": "1.8.1", + "cors": "2.8.5", + "express": "4.17.3", + "invoices": "2.0.5", + "lightning": "5.10.1", + "macaroon": "3.0.4", + "morgan": "1.10.0", + "ws": "8.5.0" + } + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "macaroon": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/macaroon/-/macaroon-3.0.4.tgz", + "integrity": "sha512-Tja2jvupseKxltPZbu5RPSz2Pgh6peYA3O46YCTcYL8PI1VqtGwDqRhGfP8pows26xx9wTiygk+en62Bq+Y8JA==", + "requires": { + "sjcl": "^1.0.6", + "tweetnacl": "^1.0.0", + "tweetnacl-util": "^0.15.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "mysql2": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", + "requires": { + "denque": "^2.0.1", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "named-placeholders": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", + "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", + "requires": { + "lru-cache": "^4.1.3" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "protobufjs": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "psbt": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.0.1.tgz", + "integrity": "sha512-4s5DSXDJ9xAYjuRJXf8rEuqs+Leyl11TE3y98xzlhMJN2UYXSLkAW1KSUdm/gdu1cSTcdcicIFZscNXmxFko+w==", + "requires": { + "bip66": "1.1.5", + "bitcoin-ops": "1.4.1", + "bitcoinjs-lib": "6.0.1", + "bn.js": "5.2.0", + "pushdata-bitcoin": "1.0.1", + "varuint-bitcoin": "1.1.2" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "requires": { + "bitcoin-ops": "^1.3.0" + } + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" + }, + "serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "sjcl": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", + "integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==" + }, + "sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tiny-secp256k1": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", + "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", + "requires": { + "uint8array-tools": "0.0.7" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" + }, + "type-fest": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.2.tgz", + "integrity": "sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==" + }, + "uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "requires": { + "bs58check": "<3.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } + } +} diff --git a/lightning-backend/package.json b/lightning-backend/package.json new file mode 100644 index 000000000..a44c9d82f --- /dev/null +++ b/lightning-backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "lightning-backend", + "version": "1.0.0", + "description": "Backend for the Mempool Lightning Explorer", + "license": "AGPL-3.0", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "tsc": "./node_modules/typescript/bin/tsc", + "build": "npm run tsc", + "start": "node --max-old-space-size=2048 dist/index.js" + }, + "author": "", + "devDependencies": { + "@types/express": "^4.17.13", + "@types/node": "^17.0.24" + }, + "dependencies": { + "axios": "^0.27.2", + "express": "^4.17.3", + "ln-service": "^53.11.0", + "mysql2": "^2.3.3", + "typescript": "^4.6.3" + } +} diff --git a/lightning-backend/tsconfig.json b/lightning-backend/tsconfig.json new file mode 100644 index 000000000..8b4cbea2e --- /dev/null +++ b/lightning-backend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "lib": ["es2019", "dom"], + "strict": true, + "noImplicitAny": false, + "sourceMap": false, + "outDir": "dist", + "moduleResolution": "node", + "typeRoots": [ + "node_modules/@types" + ], + "allowSyntheticDefaultImports": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "dist/**" + ] +} \ No newline at end of file diff --git a/lightning-backend/tslint.json b/lightning-backend/tslint.json new file mode 100644 index 000000000..945512322 --- /dev/null +++ b/lightning-backend/tslint.json @@ -0,0 +1,137 @@ +{ + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": false, + "import-blacklist": [ + true, + "rxjs", + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": false, + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} From 1f6008f269bccc17cfef3d63ced91f0a1d94ea4b Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 3 Jul 2022 20:37:01 +0200 Subject: [PATCH 36/51] Store channel closing date --- lightning-backend/src/database-migration.ts | 1 + lightning-backend/src/tasks/node-sync.service.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lightning-backend/src/database-migration.ts b/lightning-backend/src/database-migration.ts index bedc2f856..ebf149ac9 100644 --- a/lightning-backend/src/database-migration.ts +++ b/lightning-backend/src/database-migration.ts @@ -215,6 +215,7 @@ class DatabaseMigration { created datetime DEFAULT NULL, status int(11) NOT NULL DEFAULT 0, closing_transaction_id varchar(64) DEFAULT NULL, + closing_date datetime DEFAULT NULL, closing_reason int(11) DEFAULT NULL, node1_public_key varchar(66) NOT NULL, node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 3e6bfc96c..fcf3da093 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -119,7 +119,8 @@ class NodeSyncService { const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); if (spendingTx.spent === true && spendingTx.status?.confirmed === true) { logger.debug('Marking channel: ' + channel.id + ' as closed.'); - await DB.query(`UPDATE channels SET status = 2 WHERE id = ?`, [channel.id]); + await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`, + [spendingTx.status.block_time, channel.id]); if (spendingTx.txid && !channel.closing_transaction_id) { await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]); } From faafa6db3b19eb48e93a82e13e1159d109b4cc96 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 4 Jul 2022 12:00:16 +0200 Subject: [PATCH 37/51] Backfill node and capacity --- lightning-backend/src/index.ts | 8 +- .../src/tasks/node-sync.service.ts | 5 +- .../src/tasks/stats-updater.service.ts | 105 ++++++++++++++++-- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/lightning-backend/src/index.ts b/lightning-backend/src/index.ts index 614ac8499..fc7a95f2d 100644 --- a/lightning-backend/src/index.ts +++ b/lightning-backend/src/index.ts @@ -1,13 +1,9 @@ -import config from './config'; -import logger from './logger'; import DB from './database'; import databaseMigration from './database-migration'; import statsUpdater from './tasks/stats-updater.service'; import nodeSyncService from './tasks/node-sync.service'; import server from './server'; -logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); - class LightningServer { constructor() { this.init(); @@ -17,8 +13,8 @@ class LightningServer { await DB.checkDbConnection(); await databaseMigration.$initializeOrMigrateDatabase(); - statsUpdater.startService(); - nodeSyncService.startService(); + nodeSyncService.$startService(); + statsUpdater.$startService(); server.startServer(); } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index fcf3da093..095e9eeac 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -8,15 +8,14 @@ import bitcoinClient from '../api/bitcoin/bitcoin-client'; import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; import config from '../config'; import { IEsploraApi } from '../api/bitcoin/esplora-api.interface'; -import e from 'express'; class NodeSyncService { constructor() {} - public async startService() { + public async $startService() { logger.info('Starting node sync service'); - this.$updateNodes(); + await this.$updateNodes(); setInterval(async () => { await this.$updateNodes(); diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/lightning-backend/src/tasks/stats-updater.service.ts index ced094a66..150851a81 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/lightning-backend/src/tasks/stats-updater.service.ts @@ -6,22 +6,26 @@ import lightningApi from '../api/lightning/lightning-api-factory'; class LightningStatsUpdater { constructor() {} - public async startService() { + public async $startService() { logger.info('Starting Stats service'); 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(() => { - this.$logLightningStats(); - setInterval(() => { - this.$logLightningStats(); - this.$logNodeStatsDaily(); + // setTimeout(() => { + setInterval(async () => { + await this.$runTasks(); }, 1000 * 60 * 60); - }, difference); + //}, difference); - this.$logNodeStatsDaily(); + await this.$runTasks(); + } + + private async $runTasks() { + await this.$populateHistoricalData(); + await this.$logLightningStatsDaily(); + await this.$logNodeStatsDaily(); } private async $logNodeStatsDaily() { @@ -54,8 +58,91 @@ class LightningStatsUpdater { } } - private async $logLightningStats() { + // We only run this on first launch + private async $populateHistoricalData() { + const startTime = '2018-01-13'; try { + const [rows]: any = await DB.query(`SELECT COUNT(*) FROM statistics`); + // Only store once per day + if (rows[0]['COUNT(*)'] > 0) { + return; + } + 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++; + } + + const query = `INSERT INTO statistics( + added, + channel_count, + node_count, + total_capacity + ) + VALUES (FROM_UNIXTIME(?), ?, ?, ?)`; + + await DB.query(query, [ + date.getTime() / 1000, + channelsCount, + 0, + totalCapacity, + ]); + + // Add one day and continue + date.setDate(date.getDate() + 1); + } + + const [nodes]: any = await DB.query(`SELECT first_seen FROM nodes ORDER BY first_seen ASC`); + date = new Date(startTime); + + while (date < currentDate) { + let nodeCount = 0; + for (const node of nodes) { + if (new Date(node.first_seen) > date) { + break; + } + nodeCount++; + } + + const query = `UPDATE statistics SET node_count = ? WHERE added = FROM_UNIXTIME(?)`; + + await DB.query(query, [ + nodeCount, + date.getTime() / 1000, + ]); + + // Add one day and continue + date.setDate(date.getDate() + 1); + } + + logger.debug('Historical stats populated.'); + } 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; + } + const networkGraph = await lightningApi.$getNetworkGraph(); let total_capacity = 0; for (const channel of networkGraph.channels) { From a238420d7f48ff783c13834d9152153c43c73ccb Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 6 Jul 2022 11:58:06 +0200 Subject: [PATCH 38/51] Merge Lightning backend into Mempool backend --- backend/mempool-config.sample.json | 9 + backend/package.json | 1 + .../bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 14 +- .../src/api/bitcoin/esplora-api.interface.ts | 6 +- backend/src/api/bitcoin/esplora-api.ts | 5 + backend/src/api/database-migration.ts | 87 +- .../src/api/explorer/channels.api.ts | 2 +- .../src/api/explorer/channels.routes.ts | 12 +- .../src/api/explorer/general.routes.ts | 24 +- .../src/api/explorer/nodes.api.ts | 14 - .../src/api/explorer/nodes.routes.ts | 24 +- backend/src/api/explorer/statistics.api.ts | 32 + .../lightning-api-abstract-factory.ts | 0 .../api/lightning/lightning-api-factory.ts | 2 +- .../api/lightning/lightning-api.interface.ts | 0 .../src/api/lightning/lnd/lnd-api.ts | 9 +- backend/src/config.ts | 26 +- backend/src/index.ts | 18 + .../src/tasks/lightning}/node-sync.service.ts | 50 +- .../tasks/lightning}/stats-updater.service.ts | 28 +- frontend/proxy.conf.local.js | 6 +- .../app/lightning/lightning-api.service.ts | 30 +- frontend/src/app/services/api.service.ts | 4 +- lightning-backend/.gitignore | 48 - lightning-backend/mempool-config.sample.json | 38 - lightning-backend/package-lock.json | 3291 ----------------- lightning-backend/package.json | 25 - .../bitcoin/bitcoin-api-abstract-factory.ts | 25 - .../src/api/bitcoin/bitcoin-api-factory.ts | 15 - .../src/api/bitcoin/bitcoin-api.interface.ts | 175 - .../src/api/bitcoin/bitcoin-api.ts | 313 -- .../src/api/bitcoin/bitcoin-client.ts | 12 - .../src/api/bitcoin/esplora-api.interface.ts | 172 - .../src/api/bitcoin/esplora-api.ts | 84 - .../src/api/bitcoin/rpc-api/commands.ts | 92 - .../src/api/bitcoin/rpc-api/index.ts | 61 - .../src/api/bitcoin/rpc-api/jsonrpc.ts | 162 - .../src/api/explorer/statistics.api.ts | 17 - lightning-backend/src/config.ts | 110 - lightning-backend/src/database-migration.ts | 260 -- lightning-backend/src/database.ts | 51 - lightning-backend/src/index.ts | 23 - lightning-backend/src/logger.ts | 145 - lightning-backend/src/server.ts | 40 - lightning-backend/tsconfig.json | 22 - lightning-backend/tslint.json | 137 - 47 files changed, 298 insertions(+), 5424 deletions(-) rename {lightning-backend => backend}/src/api/explorer/channels.api.ts (99%) rename {lightning-backend => backend}/src/api/explorer/channels.routes.ts (85%) rename {lightning-backend => backend}/src/api/explorer/general.routes.ts (58%) rename {lightning-backend => backend}/src/api/explorer/nodes.api.ts (86%) rename {lightning-backend => backend}/src/api/explorer/nodes.routes.ts (65%) create mode 100644 backend/src/api/explorer/statistics.api.ts rename {lightning-backend => backend}/src/api/lightning/lightning-api-abstract-factory.ts (100%) rename {lightning-backend => backend}/src/api/lightning/lightning-api-factory.ts (88%) rename {lightning-backend => backend}/src/api/lightning/lightning-api.interface.ts (100%) rename {lightning-backend => backend}/src/api/lightning/lnd/lnd-api.ts (79%) rename {lightning-backend/src/tasks => backend/src/tasks/lightning}/node-sync.service.ts (92%) rename {lightning-backend/src/tasks => backend/src/tasks/lightning}/stats-updater.service.ts (85%) delete mode 100644 lightning-backend/.gitignore delete mode 100644 lightning-backend/mempool-config.sample.json delete mode 100644 lightning-backend/package-lock.json delete mode 100644 lightning-backend/package.json delete mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts delete mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts delete mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts delete mode 100644 lightning-backend/src/api/bitcoin/bitcoin-api.ts delete mode 100644 lightning-backend/src/api/bitcoin/bitcoin-client.ts delete mode 100644 lightning-backend/src/api/bitcoin/esplora-api.interface.ts delete mode 100644 lightning-backend/src/api/bitcoin/esplora-api.ts delete mode 100644 lightning-backend/src/api/bitcoin/rpc-api/commands.ts delete mode 100644 lightning-backend/src/api/bitcoin/rpc-api/index.ts delete mode 100644 lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts delete mode 100644 lightning-backend/src/api/explorer/statistics.api.ts delete mode 100644 lightning-backend/src/config.ts delete mode 100644 lightning-backend/src/database-migration.ts delete mode 100644 lightning-backend/src/database.ts delete mode 100644 lightning-backend/src/index.ts delete mode 100644 lightning-backend/src/logger.ts delete mode 100644 lightning-backend/src/server.ts delete mode 100644 lightning-backend/tsconfig.json delete mode 100644 lightning-backend/tslint.json diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index eedbf3e4c..9fe8ca36a 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -66,6 +66,15 @@ "ENABLED": false, "DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db" }, + "LIGHTNING": { + "ENABLED": false, + "BACKEND": "lnd" + }, + "LND_NODE_AUTH": { + "TLS_CERT_PATH": "tls.cert", + "MACAROON_PATH": "admin.macaroon", + "SOCKET": "localhost:10009" + }, "SOCKS5PROXY": { "ENABLED": false, "USE_ONION": true, diff --git a/backend/package.json b/backend/package.json index d3fb90e21..d2d14fb0a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", + "ln-service": "^53.17.4", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.5.1", "socks-proxy-agent": "~7.0.0", diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 9802bcd71..f3f7011dd 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -13,6 +13,7 @@ export interface AbstractBitcoinApi { $getAddressTransactions(address: string, lastSeenTxId: string): Promise; $getAddressPrefix(prefix: string): string[]; $sendRawTransaction(rawTransaction: string): Promise; + $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; } diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 7309256bd..3152954c1 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -130,6 +130,16 @@ class BitcoinApi implements AbstractBitcoinApi { return this.bitcoindClient.sendRawTransaction(rawTransaction); } + async $getOutspend(txId: string, vout: number): Promise { + const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); + return { + spent: txOut === null, + status: { + confirmed: true, + } + }; + } + async $getOutspends(txId: string): Promise { const outSpends: IEsploraApi.Outspend[] = []; const tx = await this.$getRawTransaction(txId, true, false); @@ -195,7 +205,9 @@ class BitcoinApi implements AbstractBitcoinApi { sequence: vin.sequence, txid: vin.txid || '', vout: vin.vout || 0, - witness: vin.txinwitness, + witness: vin.txinwitness || [], + inner_redeemscript_asm: '', + inner_witnessscript_asm: '', }; }); diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index f825c60f9..39f8cfd6f 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -25,10 +25,10 @@ export namespace IEsploraApi { is_coinbase: boolean; scriptsig: string; scriptsig_asm: string; - inner_redeemscript_asm?: string; - inner_witnessscript_asm?: string; + inner_redeemscript_asm: string; + inner_witnessscript_asm: string; sequence: any; - witness?: string[]; + witness: string[]; prevout: Vout | null; // Elements is_pegin?: boolean; diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 007b4131c..e8eee343a 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -66,6 +66,11 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } + $getOutspend(txId: string, vout: number): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) + .then((response) => response.data); + } + $getOutspends(txId: string): Promise { return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) .then((response) => response.data); diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index c4107e426..85a10b34b 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 24; + private static currentVersion = 25; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -248,6 +248,15 @@ class DatabaseMigration { await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`'); await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits')); } + + if (databaseSchemaVersion < 25 && isBitcoin === true) { + await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`); + await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats')); + await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); + await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); + await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); + } + } catch (e) { throw e; } @@ -569,6 +578,82 @@ class DatabaseMigration { adjustment float NOT NULL, PRIMARY KEY (height), INDEX (time) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateLightningStatisticsQuery(): string { + return `CREATE TABLE IF NOT EXISTS lightning_stats ( + id int(11) NOT NULL AUTO_INCREMENT, + added datetime NOT NULL, + channel_count int(11) NOT NULL, + node_count int(11) NOT NULL, + total_capacity double unsigned NOT NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateNodesQuery(): string { + return `CREATE TABLE IF NOT EXISTS nodes ( + public_key varchar(66) NOT NULL, + first_seen datetime NOT NULL, + updated_at datetime NOT NULL, + alias varchar(200) CHARACTER SET utf8mb4 NOT NULL, + color varchar(200) NOT NULL, + sockets text DEFAULT NULL, + PRIMARY KEY (public_key), + KEY alias (alias(10)) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateChannelsQuery(): string { + return `CREATE TABLE IF NOT EXISTS channels ( + id bigint(11) unsigned NOT NULL, + short_id varchar(15) NOT NULL DEFAULT '', + capacity bigint(20) unsigned NOT NULL, + transaction_id varchar(64) NOT NULL, + transaction_vout int(11) NOT NULL, + updated_at datetime DEFAULT NULL, + created datetime DEFAULT NULL, + status int(11) NOT NULL DEFAULT 0, + closing_transaction_id varchar(64) DEFAULT NULL, + closing_date datetime DEFAULT NULL, + closing_reason int(11) DEFAULT NULL, + node1_public_key varchar(66) NOT NULL, + node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, + node1_cltv_delta int(11) DEFAULT NULL, + node1_fee_rate bigint(11) DEFAULT NULL, + node1_is_disabled tinyint(1) DEFAULT NULL, + node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node1_min_htlc_mtokens bigint(20) DEFAULT NULL, + node1_updated_at datetime DEFAULT NULL, + node2_public_key varchar(66) NOT NULL, + node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, + node2_cltv_delta int(11) DEFAULT NULL, + node2_fee_rate bigint(11) DEFAULT NULL, + node2_is_disabled tinyint(1) DEFAULT NULL, + node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL, + node2_updated_at datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY node1_public_key (node1_public_key), + KEY node2_public_key (node2_public_key), + KEY status (status), + KEY short_id (short_id), + KEY transaction_id (transaction_id), + KEY closing_transaction_id (closing_transaction_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateNodesStatsQuery(): string { + return `CREATE TABLE IF NOT EXISTS node_stats ( + id int(11) unsigned NOT NULL AUTO_INCREMENT, + public_key varchar(66) NOT NULL DEFAULT '', + added date NOT NULL, + capacity bigint(20) unsigned NOT NULL DEFAULT 0, + channels int(11) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY added (added,public_key), + KEY public_key (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } diff --git a/lightning-backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts similarity index 99% rename from lightning-backend/src/api/explorer/channels.api.ts rename to backend/src/api/explorer/channels.api.ts index c72f93257..67d2d38e0 100644 --- a/lightning-backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -38,7 +38,7 @@ class ChannelsApi { public async $getClosedChannelsWithoutReason(): Promise { try { - const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL`; + const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL AND closing_transaction_id != ''`; const [rows]: any = await DB.query(query); return rows; } catch (e) { diff --git a/lightning-backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts similarity index 85% rename from lightning-backend/src/api/explorer/channels.routes.ts rename to backend/src/api/explorer/channels.routes.ts index ff9d166f9..6735b7502 100644 --- a/lightning-backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -1,16 +1,16 @@ import config from '../../config'; -import { Express, Request, Response } from 'express'; +import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; class ChannelsRoutes { constructor() { } - public initRoutes(app: Express) { + public initRoutes(app: Application) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds) - .get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById) - .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel) - .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/txids', this.$getChannelsByTransactionIds) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode) ; } diff --git a/lightning-backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts similarity index 58% rename from lightning-backend/src/api/explorer/general.routes.ts rename to backend/src/api/explorer/general.routes.ts index d6154bdc1..26897cb79 100644 --- a/lightning-backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -1,16 +1,17 @@ import config from '../../config'; -import { Express, Request, Response } from 'express'; +import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; import statisticsApi from './statistics.api'; -class GeneralRoutes { +class GeneralLightningRoutes { constructor() { } - public initRoutes(app: Express) { + public initRoutes(app: Application) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels) - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics', this.$getStatistics) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/search', this.$searchNodesAndChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics/latest', this.$getGeneralStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics', this.$getStatistics) + ; } private async $searchNodesAndChannels(req: Request, res: Response) { @@ -38,6 +39,15 @@ class GeneralRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getGeneralStats(req: Request, res: Response) { + try { + const statistics = await statisticsApi.$getLatestStatistics(); + res.json(statistics); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } -export default new GeneralRoutes(); +export default new GeneralLightningRoutes(); diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts similarity index 86% rename from lightning-backend/src/api/explorer/nodes.api.ts rename to backend/src/api/explorer/nodes.api.ts index 4c80c7f2d..1bf9ce12d 100644 --- a/lightning-backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -46,20 +46,6 @@ class NodesApi { } } - public async $getLatestStatistics(): Promise { - try { - const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`); - const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 72`); - return { - latest: rows[0], - previous: rows2[0], - }; - } catch (e) { - logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e)); - throw e; - } - } - public async $searchNodeByPublicKeyOrAlias(search: string) { try { const searchStripped = search.replace('%', '') + '%'; diff --git a/lightning-backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts similarity index 65% rename from lightning-backend/src/api/explorer/nodes.routes.ts rename to backend/src/api/explorer/nodes.routes.ts index e21617f45..6c79c8201 100644 --- a/lightning-backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -1,17 +1,16 @@ import config from '../../config'; -import { Express, Request, Response } from 'express'; +import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; class NodesRoutes { constructor() { } - public initRoutes(app: Express) { + public initRoutes(app: Application) { app - .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/search/:search', this.$searchNode) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats) - .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) + ; } private async $searchNode(req: Request, res: Response) { @@ -45,15 +44,6 @@ class NodesRoutes { } } - private async $getGeneralStats(req: Request, res: Response) { - try { - const statistics = await nodesApi.$getLatestStatistics(); - res.json(statistics); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); - } - } - private async $getTopNodes(req: Request, res: Response) { try { const topCapacityNodes = await nodesApi.$getTopCapacityNodes(); diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts new file mode 100644 index 000000000..5dd4609b5 --- /dev/null +++ b/backend/src/api/explorer/statistics.api.ts @@ -0,0 +1,32 @@ +import logger from '../../logger'; +import DB from '../../database'; + +class StatisticsApi { + public async $getStatistics(): Promise { + try { + const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM lightning_stats ORDER BY id DESC`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getLatestStatistics(): Promise { + try { + const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`); + const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 72`); + return { + latest: rows[0], + previous: rows2[0], + }; + } catch (e) { + logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + +} + +export default new StatisticsApi(); diff --git a/lightning-backend/src/api/lightning/lightning-api-abstract-factory.ts b/backend/src/api/lightning/lightning-api-abstract-factory.ts similarity index 100% rename from lightning-backend/src/api/lightning/lightning-api-abstract-factory.ts rename to backend/src/api/lightning/lightning-api-abstract-factory.ts diff --git a/lightning-backend/src/api/lightning/lightning-api-factory.ts b/backend/src/api/lightning/lightning-api-factory.ts similarity index 88% rename from lightning-backend/src/api/lightning/lightning-api-factory.ts rename to backend/src/api/lightning/lightning-api-factory.ts index e2a730650..ab551095c 100644 --- a/lightning-backend/src/api/lightning/lightning-api-factory.ts +++ b/backend/src/api/lightning/lightning-api-factory.ts @@ -3,7 +3,7 @@ import { AbstractLightningApi } from './lightning-api-abstract-factory'; import LndApi from './lnd/lnd-api'; function lightningApiFactory(): AbstractLightningApi { - switch (config.MEMPOOL.BACKEND) { + switch (config.LIGHTNING.BACKEND) { case 'lnd': default: return new LndApi(); diff --git a/lightning-backend/src/api/lightning/lightning-api.interface.ts b/backend/src/api/lightning/lightning-api.interface.ts similarity index 100% rename from lightning-backend/src/api/lightning/lightning-api.interface.ts rename to backend/src/api/lightning/lightning-api.interface.ts diff --git a/lightning-backend/src/api/lightning/lnd/lnd-api.ts b/backend/src/api/lightning/lnd/lnd-api.ts similarity index 79% rename from lightning-backend/src/api/lightning/lnd/lnd-api.ts rename to backend/src/api/lightning/lnd/lnd-api.ts index 57f225101..87c182a42 100644 --- a/lightning-backend/src/api/lightning/lnd/lnd-api.ts +++ b/backend/src/api/lightning/lnd/lnd-api.ts @@ -8,14 +8,17 @@ import logger from '../../../logger'; class LndApi implements AbstractLightningApi { private lnd: any; constructor() { + if (!config.LIGHTNING.ENABLED) { + return; + } try { - const tls = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64'); - const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64'); + const tls = fs.readFileSync(config.LND_NODE_AUTH.TLS_CERT_PATH).toString('base64'); + const macaroon = fs.readFileSync(config.LND_NODE_AUTH.MACAROON_PATH).toString('base64'); const { lnd } = lnService.authenticatedLndGrpc({ cert: tls, macaroon: macaroon, - socket: config.LN_NODE_AUTH.SOCKET, + socket: config.LND_NODE_AUTH.SOCKET, }); this.lnd = lnd; diff --git a/backend/src/config.ts b/backend/src/config.ts index 44864d3b9..acc0b30a0 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -27,6 +27,15 @@ interface IConfig { ESPLORA: { REST_API_URL: string; }; + LIGHTNING: { + ENABLED: boolean; + BACKEND: 'lnd' | 'cln' | 'ldk'; + }; + LND_NODE_AUTH: { + TLS_CERT_PATH: string; + MACAROON_PATH: string; + SOCKET: string; + }; ELECTRUM: { HOST: string; PORT: number; @@ -158,6 +167,15 @@ const defaults: IConfig = { 'ENABLED': false, 'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db' }, + 'LIGHTNING': { + 'ENABLED': false, + 'BACKEND': 'lnd' + }, + 'LND_NODE_AUTH': { + 'TLS_CERT_PATH': '', + 'MACAROON_PATH': '', + 'SOCKET': 'localhost:10009', + }, 'SOCKS5PROXY': { 'ENABLED': false, 'USE_ONION': true, @@ -166,11 +184,11 @@ const defaults: IConfig = { 'USERNAME': '', 'PASSWORD': '' }, - "PRICE_DATA_SERVER": { + 'PRICE_DATA_SERVER': { 'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices', 'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices' }, - "EXTERNAL_DATA_SERVER": { + 'EXTERNAL_DATA_SERVER': { 'MEMPOOL_API': 'https://mempool.space/api/v1', 'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1', 'LIQUID_API': 'https://liquid.network/api/v1', @@ -190,6 +208,8 @@ class Config implements IConfig { SYSLOG: IConfig['SYSLOG']; STATISTICS: IConfig['STATISTICS']; BISQ: IConfig['BISQ']; + LIGHTNING: IConfig['LIGHTNING']; + LND_NODE_AUTH: IConfig['LND_NODE_AUTH']; SOCKS5PROXY: IConfig['SOCKS5PROXY']; PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; @@ -205,6 +225,8 @@ class Config implements IConfig { this.SYSLOG = configs.SYSLOG; this.STATISTICS = configs.STATISTICS; this.BISQ = configs.BISQ; + this.LIGHTNING = configs.LIGHTNING; + this.LND_NODE_AUTH = configs.LND_NODE_AUTH; this.SOCKS5PROXY = configs.SOCKS5PROXY; this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; diff --git a/backend/src/index.ts b/backend/src/index.ts index ff0209294..74d6fb3da 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -29,6 +29,11 @@ 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'; class Server { private wss: WebSocket.Server | undefined; @@ -130,6 +135,13 @@ class Server { bisqMarkets.startBisqService(); } + if (config.LIGHTNING.ENABLED) { + nodeSyncService.$startService() + .then(() => { + lightningStatsUpdater.$startService(); + }); + } + this.server.listen(config.MEMPOOL.HTTP_PORT, () => { if (worker) { logger.info(`Mempool Server worker #${process.pid} started`); @@ -362,6 +374,12 @@ class Server { .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth) ; } + + if (config.LIGHTNING.ENABLED) { + generalLightningRoutes.initRoutes(this.app); + nodesRoutes.initRoutes(this.app); + channelsRoutes.initRoutes(this.app); + } } } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts similarity index 92% rename from lightning-backend/src/tasks/node-sync.service.ts rename to backend/src/tasks/lightning/node-sync.service.ts index 095e9eeac..c8b7f8c4c 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -1,13 +1,13 @@ import { chanNumber } from 'bolt07'; -import DB from '../database'; -import logger from '../logger'; -import lightningApi from '../api/lightning/lightning-api-factory'; -import { ILightningApi } from '../api/lightning/lightning-api.interface'; -import channelsApi from '../api/explorer/channels.api'; -import bitcoinClient from '../api/bitcoin/bitcoin-client'; -import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; -import config from '../config'; -import { IEsploraApi } from '../api/bitcoin/esplora-api.interface'; +import DB from '../../database'; +import logger from '../../logger'; +import channelsApi from '../../api/explorer/channels.api'; +import bitcoinClient from '../../api/bitcoin/bitcoin-client'; +import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory'; +import config from '../../config'; +import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; +import lightningApi from '../../api/lightning/lightning-api-factory'; +import { ILightningApi } from '../../api/lightning/lightning-api.interface'; class NodeSyncService { constructor() {} @@ -15,43 +15,36 @@ class NodeSyncService { public async $startService() { logger.info('Starting node sync service'); - await this.$updateNodes(); + await this.$runUpdater(); setInterval(async () => { - await this.$updateNodes(); + await this.$runUpdater(); }, 1000 * 60 * 60); } - private async $updateNodes() { + private async $runUpdater() { try { + logger.info(`Updating nodes and channels...`); + const networkGraph = await lightningApi.$getNetworkGraph(); for (const node of networkGraph.nodes) { await this.$saveNode(node); } - logger.debug(`Nodes updated`); + logger.info(`Nodes updated.`); await this.$setChannelsInactive(); for (const channel of networkGraph.channels) { await this.$saveChannel(channel); } - logger.debug(`Channels updated`); + logger.info(`Channels updated.`); await this.$findInactiveNodesAndChannels(); - logger.debug(`Inactive channels scan complete`); - await this.$lookUpCreationDateFromChain(); - logger.debug(`Channel creation dates scan complete`); - await this.$updateNodeFirstSeen(); - logger.debug(`Node first seen dates scan complete`); - await this.$scanForClosedChannels(); - logger.debug(`Closed channels scan complete`); - await this.$runClosedChannelsForensics(); - logger.debug(`Closed channels forensics scan complete`); } catch (e) { logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); @@ -80,18 +73,21 @@ class NodeSyncService { await DB.query(query, params); } } + logger.info(`Node first seen dates scan complete.`); } catch (e) { logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e)); } } private async $lookUpCreationDateFromChain() { + logger.info(`Running channel creation date lookup...`); try { const channels = await channelsApi.$getChannelsWithoutCreatedDate(); for (const channel of channels) { const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1); await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]); } + logger.info(`Channel creation dates scan complete.`); } catch (e) { logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e)); } @@ -99,6 +95,8 @@ class NodeSyncService { // Looking for channels whos nodes are inactive private async $findInactiveNodesAndChannels(): Promise { + logger.info(`Running inactive channels scan...`); + try { // @ts-ignore const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`); @@ -106,6 +104,7 @@ class NodeSyncService { for (const channel of channels) { await this.$updateChannelStatus(channel.id, 0); } + logger.info(`Inactive channels scan complete.`); } catch (e) { logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e)); } @@ -113,6 +112,7 @@ class NodeSyncService { private async $scanForClosedChannels(): Promise { try { + logger.info(`Starting closed channels scan...`); const channels = await channelsApi.$getChannelsByStatus(0); for (const channel of channels) { const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); @@ -125,6 +125,7 @@ class NodeSyncService { } } } + logger.info(`Closed channels scan complete.`); } catch (e) { logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); } @@ -140,8 +141,8 @@ class NodeSyncService { if (!config.ESPLORA.REST_API_URL) { return; } - try { + logger.info(`Started running closed channel forensics...`); const channels = await channelsApi.$getClosedChannelsWithoutReason(); for (const channel of channels) { let reason = 0; @@ -186,6 +187,7 @@ class NodeSyncService { await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]); } } + logger.info(`Closed channels forensics scan complete.`); } catch (e) { logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); } diff --git a/lightning-backend/src/tasks/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts similarity index 85% rename from lightning-backend/src/tasks/stats-updater.service.ts rename to backend/src/tasks/lightning/stats-updater.service.ts index 150851a81..83b9973d4 100644 --- a/lightning-backend/src/tasks/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -1,7 +1,6 @@ - -import DB from '../database'; -import logger from '../logger'; -import lightningApi from '../api/lightning/lightning-api-factory'; +import logger from "../../logger"; +import DB from "../../database"; +import lightningApi from "../../api/lightning/lightning-api-factory"; class LightningStatsUpdater { constructor() {} @@ -29,6 +28,8 @@ 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'`); @@ -52,7 +53,7 @@ class LightningStatsUpdater { node.channels_count_left + node.channels_count_right]); } await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]); - logger.debug('Daily node stats has updated.'); + logger.info('Daily node stats has updated.'); } catch (e) { logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } @@ -60,9 +61,11 @@ 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 statistics`); + const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`); // Only store once per day if (rows[0]['COUNT(*)'] > 0) { return; @@ -86,7 +89,7 @@ class LightningStatsUpdater { channelsCount++; } - const query = `INSERT INTO statistics( + const query = `INSERT INTO lightning_stats( added, channel_count, node_count, @@ -117,7 +120,7 @@ class LightningStatsUpdater { nodeCount++; } - const query = `UPDATE statistics SET node_count = ? WHERE added = FROM_UNIXTIME(?)`; + const query = `UPDATE lightning_stats SET node_count = ? WHERE added = FROM_UNIXTIME(?)`; await DB.query(query, [ nodeCount, @@ -128,13 +131,15 @@ class LightningStatsUpdater { date.setDate(date.getDate() + 1); } - logger.debug('Historical stats populated.'); + logger.info('Historical stats populated.'); } catch (e) { logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e)); } } 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'`); @@ -151,7 +156,7 @@ class LightningStatsUpdater { } } - const query = `INSERT INTO statistics( + const query = `INSERT INTO lightning_stats( added, channel_count, node_count, @@ -164,8 +169,9 @@ class LightningStatsUpdater { networkGraph.nodes.length, total_capacity, ]); + logger.info(`Lightning daily stats done.`); } catch (e) { - logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e)); + logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } } } diff --git a/frontend/proxy.conf.local.js b/frontend/proxy.conf.local.js index 9be0ed770..b2fb1bb27 100644 --- a/frontend/proxy.conf.local.js +++ b/frontend/proxy.conf.local.js @@ -103,13 +103,13 @@ if (configContent && configContent.BASE_MODULE === 'bisq') { PROXY_CONFIG.push(...[ { - context: ['/lightning/api/v1/**'], - target: `http://localhost:8899`, + context: ['/testnet/api/v1/lightning/**'], + target: `http://localhost:8999`, secure: false, changeOrigin: true, proxyTimeout: 30000, pathRewrite: { - "^/lightning/api": "/api" + "^/testnet": "" }, }, { diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 5a6e63305..7157c9bd7 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -1,23 +1,33 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; - -const API_BASE_URL = '/lightning/api/v1'; +import { StateService } from '../services/state.service'; @Injectable({ providedIn: 'root' }) export class LightningApiService { + private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet + constructor( private httpClient: HttpClient, - ) { } + private stateService: StateService, + ) { + this.apiBasePath = ''; // assume mainnet by default + this.stateService.networkChanged$.subscribe((network) => { + if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) { + network = ''; + } + this.apiBasePath = network ? '/' + network : ''; + }); + } getNode$(publicKey: string): Observable { - return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey); } getChannel$(shortId: string): Observable { - return this.httpClient.get(API_BASE_URL + '/channels/' + shortId); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/channels/' + shortId); } getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable { @@ -27,22 +37,22 @@ export class LightningApiService { .set('status', status) ; - return this.httpClient.get(API_BASE_URL + '/channels', { params, observe: 'response' }); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' }); } getLatestStatistics$(): Observable { - return this.httpClient.get(API_BASE_URL + '/statistics/latest'); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/statistics/latest'); } listNodeStats$(publicKey: string): Observable { - return this.httpClient.get(API_BASE_URL + '/nodes/' + publicKey + '/statistics'); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics'); } listTopNodes$(): Observable { - return this.httpClient.get(API_BASE_URL + '/nodes/top'); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/top'); } listStatistics$(): Observable { - return this.httpClient.get(API_BASE_URL + '/statistics'); + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/statistics'); } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index f728e64d4..ddeb538d9 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -237,12 +237,12 @@ export class ApiService { txIds.forEach((txId: string) => { params = params.append('txId[]', txId); }); - return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params }); + return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params }); } lightningSearch$(searchText: string): Observable { let params = new HttpParams().set('searchText', searchText); - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/search', { params }); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params }); } } diff --git a/lightning-backend/.gitignore b/lightning-backend/.gitignore deleted file mode 100644 index 3a41effef..000000000 --- a/lightning-backend/.gitignore +++ /dev/null @@ -1,48 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# production config and external assets -*.json -!mempool-config.sample.json -!package.json -!package-lock.json -!tslint.json -!tsconfig.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 deleted file mode 100644 index 2a2d3d6a9..000000000 --- a/lightning-backend/mempool-config.sample.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "MEMPOOL": { - "NETWORK": "mainnet", - "BACKEND": "lnd", - "HTTP_PORT": 8899, - "API_URL_PREFIX": "/api/v1/", - "STDOUT_LOG_MIN_PRIORITY": "debug" - }, - "ESPLORA": { - "REST_API_URL": "" - }, - "SYSLOG": { - "ENABLED": false, - "HOST": "127.0.0.1", - "PORT": 514, - "MIN_PRIORITY": "info", - "FACILITY": "local7" - }, - "LN_NODE_AUTH": { - "TLS_CERT_PATH": "", - "MACAROON_PATH": "", - "SOCKET": "localhost:10009" - }, - "CORE_RPC": { - "HOST": "127.0.0.1", - "PORT": 8332, - "USERNAME": "mempool", - "PASSWORD": "mempool" - }, - "DATABASE": { - "HOST": "127.0.0.1", - "PORT": 3306, - "SOCKET": "/var/run/mysql/mysql.sock", - "DATABASE": "lightning", - "USERNAME": "root", - "PASSWORD": "root" - } -} diff --git a/lightning-backend/package-lock.json b/lightning-backend/package-lock.json deleted file mode 100644 index 38a1a4571..000000000 --- a/lightning-backend/package-lock.json +++ /dev/null @@ -1,3291 +0,0 @@ -{ - "name": "lightning-backend", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "lightning-backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "axios": "^0.27.2", - "express": "^4.17.3", - "ln-service": "^53.11.0", - "mysql2": "^2.3.3", - "typescript": "^4.6.3" - }, - "devDependencies": { - "@types/express": "^4.17.13", - "@types/node": "^17.0.24" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "node_modules/@types/node": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", - "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, - "node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - }, - "node_modules/asyncjs-util": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", - "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", - "dependencies": { - "async": "3.2.3" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "node_modules/bip174": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", - "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/bitcoin-ops": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", - "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" - }, - "node_modules/bitcoinjs-lib": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", - "integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==", - "dependencies": { - "bech32": "^2.0.0", - "bip174": "^2.0.1", - "bs58check": "^2.1.2", - "create-hash": "^1.1.0", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.1.2", - "wif": "^2.0.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, - "node_modules/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/bolt07": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", - "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", - "dependencies": { - "bn.js": "5.2.0" - } - }, - "node_modules/bolt09": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.2.tgz", - "integrity": "sha512-m533YWZ/R/p1buxEK/19v94Ay1vS1PJNwfP30BCVj6l96NGpOa9t40HYuMpoX+xFYwOx8kZs+GGTb9TbJund0w==", - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", - "dependencies": { - "nofilter": "^3.1.0" - }, - "engines": { - "node": ">=12.19" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/denque": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", - "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "node_modules/ecpair": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", - "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", - "dependencies": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.19.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.2", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.9.7", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dependencies": { - "is-property": "^1.0.2" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/invoices": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.5.tgz", - "integrity": "sha512-097isfZK3qaDJXQOEqTr3IfnrFZnGCAsbyqWNHAESdG12vBC39dprZWFwPLtnv7I8exhJG6WFFlaC51qaJan/w==", - "dependencies": { - "bech32": "2.0.0", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "bolt07": "1.8.1", - "bolt09": "0.2.2", - "tiny-secp256k1": "2.2.1" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "node_modules/lightning": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.10.1.tgz", - "integrity": "sha512-dIrN4vPzmzq9DaMD6c+9DqQwJCMl1lOleWrhIrv+HIpzq6rdNJvUXaVJOFz1OV8P3zy2Q3+s9VxnzeN70ee+ow==", - "dependencies": { - "@grpc/grpc-js": "1.6.2", - "@grpc/proto-loader": "0.6.9", - "@types/express": "4.17.13", - "@types/node": "17.0.23", - "@types/request": "2.48.8", - "@types/ws": "8.5.3", - "async": "3.2.3", - "asyncjs-util": "1.2.9", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "body-parser": "1.20.0", - "bolt07": "1.8.1", - "bolt09": "0.2.3", - "cbor": "8.1.0", - "ecpair": "2.0.1", - "express": "4.17.3", - "invoices": "2.0.5", - "psbt": "2.0.1", - "tiny-secp256k1": "2.2.1", - "type-fest": "2.12.2" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/lightning/node_modules/@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", - "dependencies": { - "@grpc/proto-loader": "^0.6.4", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/lightning/node_modules/@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lightning/node_modules/@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" - }, - "node_modules/lightning/node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/lightning/node_modules/bolt09": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", - "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/lightning/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/lightning/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/lightning/node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/lightning/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/lightning/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/lightning/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/lightning/node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/lightning/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/lightning/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ln-service": { - "version": "53.11.0", - "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-53.11.0.tgz", - "integrity": "sha512-qdsgLRFGdn8+zfSDgbGw584fS2QQromxp4VRXzj9nk3qveTD6IwBjaEhC1xtY73MQCHQ3ALkWVn3aYMoy5erFw==", - "dependencies": { - "bolt07": "1.8.1", - "cors": "2.8.5", - "express": "4.17.3", - "invoices": "2.0.5", - "lightning": "5.10.1", - "macaroon": "3.0.4", - "morgan": "1.10.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/macaroon": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/macaroon/-/macaroon-3.0.4.tgz", - "integrity": "sha512-Tja2jvupseKxltPZbu5RPSz2Pgh6peYA3O46YCTcYL8PI1VqtGwDqRhGfP8pows26xx9wTiygk+en62Bq+Y8JA==", - "dependencies": { - "sjcl": "^1.0.6", - "tweetnacl": "^1.0.0", - "tweetnacl-util": "^0.15.0" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/mysql2": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", - "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", - "dependencies": { - "denque": "^2.0.1", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^4.0.0", - "lru-cache": "^6.0.0", - "named-placeholders": "^1.1.2", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", - "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", - "dependencies": { - "lru-cache": "^4.1.3" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/named-placeholders/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/named-placeholders/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "engines": { - "node": ">=12.19" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psbt": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.0.1.tgz", - "integrity": "sha512-4s5DSXDJ9xAYjuRJXf8rEuqs+Leyl11TE3y98xzlhMJN2UYXSLkAW1KSUdm/gdu1cSTcdcicIFZscNXmxFko+w==", - "dependencies": { - "bip66": "1.1.5", - "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "pushdata-bitcoin": "1.0.1", - "varuint-bitcoin": "1.1.2" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "node_modules/pushdata-bitcoin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", - "dependencies": { - "bitcoin-ops": "^1.3.0" - } - }, - "node_modules/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" - }, - "node_modules/serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sjcl": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", - "integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==", - "engines": { - "node": "*" - } - }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tiny-secp256k1": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", - "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", - "dependencies": { - "uint8array-tools": "0.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" - }, - "node_modules/type-fest": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.2.tgz", - "integrity": "sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" - }, - "node_modules/typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wif": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", - "dependencies": { - "bs58check": "<3.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - } - }, - "dependencies": { - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "@types/node": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", - "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, - "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "requires": { - "@types/node": "*" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - }, - "asyncjs-util": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", - "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", - "requires": { - "async": "3.2.3" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "bip174": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", - "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" - }, - "bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "bitcoin-ops": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", - "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" - }, - "bitcoinjs-lib": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", - "integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==", - "requires": { - "bech32": "^2.0.0", - "bip174": "^2.0.1", - "bs58check": "^2.1.2", - "create-hash": "^1.1.0", - "typeforce": "^1.11.3", - "varuint-bitcoin": "^1.1.2", - "wif": "^2.0.1" - } - }, - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, - "body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.9.7", - "raw-body": "2.4.3", - "type-is": "~1.6.18" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "bolt07": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.1.tgz", - "integrity": "sha512-vgh796VOdJBunZZZX0YuW1DmkS9SbW93rCLPOmWPsAHS/mStEs4+5d0KM1bYX6QBHshY9ecg4kgJaB18jrZsIA==", - "requires": { - "bn.js": "5.2.0" - } - }, - "bolt09": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.2.tgz", - "integrity": "sha512-m533YWZ/R/p1buxEK/19v94Ay1vS1PJNwfP30BCVj6l96NGpOa9t40HYuMpoX+xFYwOx8kZs+GGTb9TbJund0w==" - }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } - }, - "bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "requires": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", - "requires": { - "nofilter": "^3.1.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "denque": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", - "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "ecpair": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", - "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", - "requires": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.19.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.2", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.9.7", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "requires": { - "is-property": "^1.0.2" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "invoices": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.5.tgz", - "integrity": "sha512-097isfZK3qaDJXQOEqTr3IfnrFZnGCAsbyqWNHAESdG12vBC39dprZWFwPLtnv7I8exhJG6WFFlaC51qaJan/w==", - "requires": { - "bech32": "2.0.0", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "bolt07": "1.8.1", - "bolt09": "0.2.2", - "tiny-secp256k1": "2.2.1" - } - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "lightning": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.10.1.tgz", - "integrity": "sha512-dIrN4vPzmzq9DaMD6c+9DqQwJCMl1lOleWrhIrv+HIpzq6rdNJvUXaVJOFz1OV8P3zy2Q3+s9VxnzeN70ee+ow==", - "requires": { - "@grpc/grpc-js": "1.6.2", - "@grpc/proto-loader": "0.6.9", - "@types/express": "4.17.13", - "@types/node": "17.0.23", - "@types/request": "2.48.8", - "@types/ws": "8.5.3", - "async": "3.2.3", - "asyncjs-util": "1.2.9", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "body-parser": "1.20.0", - "bolt07": "1.8.1", - "bolt09": "0.2.3", - "cbor": "8.1.0", - "ecpair": "2.0.1", - "express": "4.17.3", - "invoices": "2.0.5", - "psbt": "2.0.1", - "tiny-secp256k1": "2.2.1", - "type-fest": "2.12.2" - }, - "dependencies": { - "@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", - "requires": { - "@grpc/proto-loader": "^0.6.4", - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.10.0", - "yargs": "^16.2.0" - } - }, - "@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" - }, - "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "bolt09": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", - "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } - } - }, - "ln-service": { - "version": "53.11.0", - "resolved": "https://registry.npmjs.org/ln-service/-/ln-service-53.11.0.tgz", - "integrity": "sha512-qdsgLRFGdn8+zfSDgbGw584fS2QQromxp4VRXzj9nk3qveTD6IwBjaEhC1xtY73MQCHQ3ALkWVn3aYMoy5erFw==", - "requires": { - "bolt07": "1.8.1", - "cors": "2.8.5", - "express": "4.17.3", - "invoices": "2.0.5", - "lightning": "5.10.1", - "macaroon": "3.0.4", - "morgan": "1.10.0", - "ws": "8.5.0" - } - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "macaroon": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/macaroon/-/macaroon-3.0.4.tgz", - "integrity": "sha512-Tja2jvupseKxltPZbu5RPSz2Pgh6peYA3O46YCTcYL8PI1VqtGwDqRhGfP8pows26xx9wTiygk+en62Bq+Y8JA==", - "requires": { - "sjcl": "^1.0.6", - "tweetnacl": "^1.0.0", - "tweetnacl-util": "^0.15.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "mysql2": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", - "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", - "requires": { - "denque": "^2.0.1", - "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", - "long": "^4.0.0", - "lru-cache": "^6.0.0", - "named-placeholders": "^1.1.2", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "named-placeholders": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", - "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", - "requires": { - "lru-cache": "^4.1.3" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - } - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "psbt": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.0.1.tgz", - "integrity": "sha512-4s5DSXDJ9xAYjuRJXf8rEuqs+Leyl11TE3y98xzlhMJN2UYXSLkAW1KSUdm/gdu1cSTcdcicIFZscNXmxFko+w==", - "requires": { - "bip66": "1.1.5", - "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.0", - "pushdata-bitcoin": "1.0.1", - "varuint-bitcoin": "1.1.2" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "pushdata-bitcoin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", - "requires": { - "bitcoin-ops": "^1.3.0" - } - }, - "qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", - "requires": { - "bytes": "3.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" - }, - "serve-static": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", - "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.2" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "sjcl": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", - "integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==" - }, - "sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "tiny-secp256k1": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", - "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", - "requires": { - "uint8array-tools": "0.0.7" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" - }, - "type-fest": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.2.tgz", - "integrity": "sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" - }, - "typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==" - }, - "uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "wif": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", - "requires": { - "bs58check": "<3.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "requires": {} - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" - } - } -} diff --git a/lightning-backend/package.json b/lightning-backend/package.json deleted file mode 100644 index a44c9d82f..000000000 --- a/lightning-backend/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "lightning-backend", - "version": "1.0.0", - "description": "Backend for the Mempool Lightning Explorer", - "license": "AGPL-3.0", - "main": "index.ts", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "tsc": "./node_modules/typescript/bin/tsc", - "build": "npm run tsc", - "start": "node --max-old-space-size=2048 dist/index.js" - }, - "author": "", - "devDependencies": { - "@types/express": "^4.17.13", - "@types/node": "^17.0.24" - }, - "dependencies": { - "axios": "^0.27.2", - "express": "^4.17.3", - "ln-service": "^53.11.0", - "mysql2": "^2.3.3", - "typescript": "^4.6.3" - } -} diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts deleted file mode 100644 index cd60843f3..000000000 --- a/lightning-backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IEsploraApi } from './esplora-api.interface'; - -export interface AbstractBitcoinApi { - $getRawMempool(): Promise; - $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; - $getBlockHeightTip(): Promise; - $getTxIdsForBlock(hash: string): Promise; - $getBlockHash(height: number): Promise; - $getBlockHeader(hash: string): Promise; - $getBlock(hash: string): Promise; - $getAddress(address: string): Promise; - $getAddressTransactions(address: string, lastSeenTxId: string): Promise; - $getAddressPrefix(prefix: string): string[]; - $sendRawTransaction(rawTransaction: string): Promise; - $getOutspend(txId: string, vout: number): Promise; - $getOutspends(txId: string): Promise; - $getBatchedOutspends(txId: string[]): Promise; -} -export interface BitcoinRpcCredentials { - host: string; - port: number; - user: string; - pass: string; - timeout: number; -} diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts b/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts deleted file mode 100644 index 3ae598ac2..000000000 --- a/lightning-backend/src/api/bitcoin/bitcoin-api-factory.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '../../config'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import EsploraApi from './esplora-api'; -import BitcoinApi from './bitcoin-api'; -import bitcoinClient from './bitcoin-client'; - -function bitcoinApiFactory(): AbstractBitcoinApi { - if (config.ESPLORA.REST_API_URL) { - return new EsploraApi(); - } else { - return new BitcoinApi(bitcoinClient); - } -} - -export default bitcoinApiFactory(); diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts b/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts deleted file mode 100644 index 54d666794..000000000 --- a/lightning-backend/src/api/bitcoin/bitcoin-api.interface.ts +++ /dev/null @@ -1,175 +0,0 @@ -export namespace IBitcoinApi { - export interface MempoolInfo { - loaded: boolean; // (boolean) True if the mempool is fully loaded - size: number; // (numeric) Current tx count - bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141. - usage: number; // (numeric) Total memory usage for the mempool - total_fee: number; // (numeric) Total fees of transactions in the mempool - maxmempool: number; // (numeric) Maximum memory usage for the mempool - mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted. - minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions - } - - export interface RawMempool { [txId: string]: MempoolEntry; } - - export interface MempoolEntry { - vsize: number; // (numeric) virtual transaction size as defined in BIP 141. - weight: number; // (numeric) transaction weight as defined in BIP 141. - time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT - height: number; // (numeric) block height when transaction entered pool - descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one) - descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one) - ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one) - ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one) - wtxid: string; // (string) hash of serialized transactionumber; including witness data - fees: { - base: number; // (numeric) transaction fee in BTC - modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC - ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC - descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC - }; - depends: string[]; // (string) parent transaction id - spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction - 'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee) - } - - export interface Block { - hash: string; // (string) the block hash (same as provided) - confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain - size: number; // (numeric) The block size - strippedsize: number; // (numeric) The block size excluding witness data - weight: number; // (numeric) The block weight as defined in BIP 141 - height: number; // (numeric) The block height or index - version: number; // (numeric) The block version - versionHex: string; // (string) The block version formatted in hexadecimal - merkleroot: string; // (string) The merkle root - tx: Transaction[]; - time: number; // (numeric) The block time expressed in UNIX epoch time - mediantime: number; // (numeric) The median block time expressed in UNIX epoch time - nonce: number; // (numeric) The nonce - bits: string; // (string) The bits - difficulty: number; // (numeric) The difficulty - chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex) - nTx: number; // (numeric) The number of transactions in the block - previousblockhash: string; // (string) The hash of the previous block - nextblockhash: string; // (string) The hash of the next block - } - - export interface Transaction { - in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not - hex: string; // (string) The serialized, hex-encoded data for 'txid' - txid: string; // (string) The transaction id (same as provided) - hash: string; // (string) The transaction hash (differs from txid for witness transactions) - size: number; // (numeric) The serialized transaction size - vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions) - weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4) - version: number; // (numeric) The version - locktime: number; // (numeric) The lock time - vin: Vin[]; - vout: Vout[]; - blockhash: string; // (string) the block hash - confirmations: number; // (numeric) The confirmations - blocktime: number; // (numeric) The block time expressed in UNIX epoch time - time: number; // (numeric) Same as blocktime - } - - export interface VerboseBlock extends Block { - tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result - } - - export interface VerboseTransaction extends Transaction { - fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available - } - - export interface Vin { - txid?: string; // (string) The transaction id - vout?: number; // (string) - scriptSig?: { // (json object) The script - asm: string; // (string) asm - hex: string; // (string) hex - }; - sequence: number; // (numeric) The script sequence number - txinwitness?: string[]; // (string) hex-encoded witness data - coinbase?: string; - is_pegin?: boolean; // (boolean) Elements peg-in - } - - export interface Vout { - value: number; // (numeric) The value in BTC - n: number; // (numeric) index - asset?: string; // (string) Elements asset id - scriptPubKey: { // (json object) - asm: string; // (string) the asm - hex: string; // (string) the hex - reqSigs?: number; // (numeric) The required sigs - type: string; // (string) The type, eg 'pubkeyhash' - address?: string; // (string) bitcoin address - addresses?: string[]; // (string) bitcoin addresses - pegout_chain?: string; // (string) Elements peg-out chain - pegout_addresses?: string[]; // (string) Elements peg-out addresses - }; - } - - export interface AddressInformation { - isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned. - isvalid_parent?: boolean; // (boolean) Elements only - address: string; // (string) The bitcoin address validated - scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address - isscript: boolean; // (boolean) If the key is a script - iswitness: boolean; // (boolean) If the address is a witness - witness_version?: number; // (numeric, optional) The version number of the witness program - witness_program: string; // (string, optional) The hex value of the witness program - confidential_key?: string; // (string) Elements only - unconfidential?: string; // (string) Elements only - } - - export interface ChainTips { - height: number; // (numeric) height of the chain tip - hash: string; // (string) block hash of the tip - branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain - status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active'; - } - - export interface BlockchainInfo { - chain: number; // (string) current network name as defined in BIP70 (main, test, regtest) - blocks: number; // (numeric) the current number of blocks processed in the server - headers: number; // (numeric) the current number of headers we have validated - bestblockhash: string, // (string) the hash of the currently best block - difficulty: number; // (numeric) the current difficulty - mediantime: number; // (numeric) median time for the current best block - verificationprogress: number; // (numeric) estimate of verification progress [0..1] - initialblockdownload: boolean; // (bool) (debug information) estimate of whether this node is in Initial Block Download mode. - chainwork: string // (string) total amount of work in active chain, in hexadecimal - size_on_disk: number; // (numeric) the estimated size of the block and undo files on disk - pruned: number; // (boolean) if the blocks are subject to pruning - pruneheight: number; // (numeric) lowest-height complete block stored (only present if pruning is enabled) - automatic_pruning: number; // (boolean) whether automatic pruning is enabled (only present if pruning is enabled) - prune_target_size: number; // (numeric) the target size used by pruning (only present if automatic pruning is enabled) - softforks: SoftFork[]; // (array) status of softforks in progress - bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress - warnings: string; // (string) any network and blockchain warnings. - } - - interface SoftFork { - id: string; // (string) name of softfork - version: number; // (numeric) block version - reject: { // (object) progress toward rejecting pre-softfork blocks - status: boolean; // (boolean) true if threshold reached - }, - } - interface Bip9SoftForks { - status: number; // (string) one of defined, started, locked_in, active, failed - bit: number; // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status) - startTime: number; // (numeric) the minimum median time past of a block at which the bit gains its meaning - timeout: number; // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in - since: number; // (numeric) height of the first block to which the status applies - statistics: { // (object) numeric statistics about BIP9 signalling for a softfork (only for started status) - period: number; // (numeric) the length in blocks of the BIP9 signalling period - threshold: number; // (numeric) the number of blocks with the version bit set required to activate the feature - elapsed: number; // (numeric) the number of blocks elapsed since the beginning of the current period - count: number; // (numeric) the number of blocks with the version bit set in the current period - possible: boolean; // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold - } - } - -} diff --git a/lightning-backend/src/api/bitcoin/bitcoin-api.ts b/lightning-backend/src/api/bitcoin/bitcoin-api.ts deleted file mode 100644 index d8fa07e80..000000000 --- a/lightning-backend/src/api/bitcoin/bitcoin-api.ts +++ /dev/null @@ -1,313 +0,0 @@ -import * as bitcoinjs from 'bitcoinjs-lib'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { IBitcoinApi } from './bitcoin-api.interface'; -import { IEsploraApi } from './esplora-api.interface'; - -class BitcoinApi implements AbstractBitcoinApi { - protected bitcoindClient: any; - - constructor(bitcoinClient: any) { - this.bitcoindClient = bitcoinClient; - } - - $getAddressPrefix(prefix: string): string[] { - throw new Error('Method not implemented.'); - } - - $getBlock(hash: string): Promise { - throw new Error('Method not implemented.'); - } - - $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise { - return this.bitcoindClient.getRawTransaction(txId, true) - .then((transaction: IBitcoinApi.Transaction) => { - if (skipConversion) { - transaction.vout.forEach((vout) => { - vout.value = Math.round(vout.value * 100000000); - }); - return transaction; - } - return this.$convertTransaction(transaction, addPrevout, lazyPrevouts); - }) - .catch((e: Error) => { - throw e; - }); - } - - $getBlockHeightTip(): Promise { - return this.bitcoindClient.getChainTips() - .then((result: IBitcoinApi.ChainTips[]) => { - return result.find(tip => tip.status === 'active')!.height; - }); - } - - $getTxIdsForBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 1) - .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); - } - - $getRawBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 0); - } - - $getBlockHash(height: number): Promise { - return this.bitcoindClient.getBlockHash(height); - } - - $getBlockHeader(hash: string): Promise { - return this.bitcoindClient.getBlockHeader(hash, false); - } - - $getAddress(address: string): Promise { - throw new Error('Method getAddress not supported by the Bitcoin RPC API.'); - } - - $getAddressTransactions(address: string, lastSeenTxId: string): Promise { - throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.'); - } - - $getRawMempool(): Promise { - return this.bitcoindClient.getRawMemPool(); - } - - $sendRawTransaction(rawTransaction: string): Promise { - return this.bitcoindClient.sendRawTransaction(rawTransaction); - } - - async $getOutspend(txId: string, vout: number): Promise { - const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); - return { - spent: txOut === null, - status: { - confirmed: true, - } - }; - } - - async $getOutspends(txId: string): Promise { - const outSpends: IEsploraApi.Outspend[] = []; - const tx = await this.$getRawTransaction(txId, true, false); - for (let i = 0; i < tx.vout.length; i++) { - if (tx.status && tx.status.block_height === 0) { - outSpends.push({ - spent: false - }); - } else { - const txOut = await this.bitcoindClient.getTxOut(txId, i); - outSpends.push({ - spent: txOut === null, - }); - } - } - return outSpends; - } - - async $getBatchedOutspends(txId: string[]): Promise { - const outspends: IEsploraApi.Outspend[][] = []; - for (const tx of txId) { - const outspend = await this.$getOutspends(tx); - outspends.push(outspend); - } - return outspends; - } - - $getEstimatedHashrate(blockHeight: number): Promise { - // 120 is the default block span in Core - return this.bitcoindClient.getNetworkHashPs(120, blockHeight); - } - - protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise { - let esploraTransaction: IEsploraApi.Transaction = { - txid: transaction.txid, - version: transaction.version, - locktime: transaction.locktime, - size: transaction.size, - weight: transaction.weight, - fee: 0, - vin: [], - vout: [], - status: { confirmed: false }, - }; - - esploraTransaction.vout = transaction.vout.map((vout) => { - return { - value: Math.round(vout.value * 100000000), - scriptpubkey: vout.scriptPubKey.hex, - scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address - : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '', - scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '', - scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type), - }; - }); - - // @ts-ignore - esploraTransaction.vin = transaction.vin.map((vin) => { - return { - is_coinbase: !!vin.coinbase, - prevout: null, - scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '', - scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '', - sequence: vin.sequence, - txid: vin.txid || '', - vout: vin.vout || 0, - witness: vin.txinwitness, - }; - }); - - if (transaction.confirmations) { - esploraTransaction.status = { - confirmed: true, - block_height: -1, - block_hash: transaction.blockhash, - block_time: transaction.blocktime, - }; - } - - if (addPrevout) { - esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts); - } else if (!transaction.confirmations) { - // esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction); - } - - return esploraTransaction; - } - - private translateScriptPubKeyType(outputType: string): string { - const map = { - 'pubkey': 'p2pk', - 'pubkeyhash': 'p2pkh', - 'scripthash': 'p2sh', - 'witness_v0_keyhash': 'v0_p2wpkh', - 'witness_v0_scripthash': 'v0_p2wsh', - 'witness_v1_taproot': 'v1_p2tr', - 'nonstandard': 'nonstandard', - 'multisig': 'multisig', - 'nulldata': 'op_return' - }; - - if (map[outputType]) { - return map[outputType]; - } else { - return 'unknown'; - } - } - - private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise { - if (transaction.vin[0].is_coinbase) { - transaction.fee = 0; - return transaction; - } - let totalIn = 0; - - for (let i = 0; i < transaction.vin.length; i++) { - if (lazyPrevouts && i > 12) { - transaction.vin[i].lazy = true; - continue; - } - const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false); - transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout]; - this.addInnerScriptsToVin(transaction.vin[i]); - totalIn += innerTx.vout[transaction.vin[i].vout].value; - } - if (lazyPrevouts && transaction.vin.length > 12) { - transaction.fee = -1; - } else { - const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0); - transaction.fee = parseFloat((totalIn - totalOut).toFixed(8)); - } - return transaction; - } - - private convertScriptSigAsm(hex: string): string { - const buf = Buffer.from(hex, 'hex'); - - const b: string[] = []; - - let i = 0; - while (i < buf.length) { - const op = buf[i]; - if (op >= 0x01 && op <= 0x4e) { - i++; - let push: number; - if (op === 0x4c) { - push = buf.readUInt8(i); - b.push('OP_PUSHDATA1'); - i += 1; - } else if (op === 0x4d) { - push = buf.readUInt16LE(i); - b.push('OP_PUSHDATA2'); - i += 2; - } else if (op === 0x4e) { - push = buf.readUInt32LE(i); - b.push('OP_PUSHDATA4'); - i += 4; - } else { - push = op; - b.push('OP_PUSHBYTES_' + push); - } - - const data = buf.slice(i, i + push); - if (data.length !== push) { - break; - } - - b.push(data.toString('hex')); - i += data.length; - } else { - if (op === 0x00) { - b.push('OP_0'); - } else if (op === 0x4f) { - b.push('OP_PUSHNUM_NEG1'); - } else if (op === 0xb1) { - b.push('OP_CLTV'); - } else if (op === 0xb2) { - b.push('OP_CSV'); - } else if (op === 0xba) { - b.push('OP_CHECKSIGADD'); - } else { - const opcode = bitcoinjs.script.toASM([ op ]); - if (opcode && op < 0xfd) { - if (/^OP_(\d+)$/.test(opcode)) { - b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1')); - } else { - b.push(opcode); - } - } else { - b.push('OP_RETURN_' + op); - } - } - i += 1; - } - } - - return b.join(' '); - } - - private addInnerScriptsToVin(vin: IEsploraApi.Vin): void { - if (!vin.prevout) { - return; - } - - if (vin.prevout.scriptpubkey_type === 'p2sh') { - const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; - vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript); - if (vin.witness && vin.witness.length > 2) { - const witnessScript = vin.witness[vin.witness.length - 1]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); - } - } - - if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) { - const witnessScript = vin.witness[vin.witness.length - 1]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); - } - - if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) { - const witnessScript = vin.witness[vin.witness.length - 2]; - vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript); - } - } - -} - -export default BitcoinApi; diff --git a/lightning-backend/src/api/bitcoin/bitcoin-client.ts b/lightning-backend/src/api/bitcoin/bitcoin-client.ts deleted file mode 100644 index 43e76a041..000000000 --- a/lightning-backend/src/api/bitcoin/bitcoin-client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from '../../config'; -const bitcoin = require('./rpc-api/index'); - -const nodeRpcCredentials: any = { - host: config.CORE_RPC.HOST, - port: config.CORE_RPC.PORT, - user: config.CORE_RPC.USERNAME, - pass: config.CORE_RPC.PASSWORD, - timeout: 60000, -}; - -export default new bitcoin.Client(nodeRpcCredentials); diff --git a/lightning-backend/src/api/bitcoin/esplora-api.interface.ts b/lightning-backend/src/api/bitcoin/esplora-api.interface.ts deleted file mode 100644 index 39f8cfd6f..000000000 --- a/lightning-backend/src/api/bitcoin/esplora-api.interface.ts +++ /dev/null @@ -1,172 +0,0 @@ -export namespace IEsploraApi { - export interface Transaction { - txid: string; - version: number; - locktime: number; - size: number; - weight: number; - fee: number; - vin: Vin[]; - vout: Vout[]; - status: Status; - hex?: string; - } - - export interface Recent { - txid: string; - fee: number; - vsize: number; - value: number; - } - - export interface Vin { - txid: string; - vout: number; - is_coinbase: boolean; - scriptsig: string; - scriptsig_asm: string; - inner_redeemscript_asm: string; - inner_witnessscript_asm: string; - sequence: any; - witness: string[]; - prevout: Vout | null; - // Elements - is_pegin?: boolean; - issuance?: Issuance; - // Custom - lazy?: boolean; - } - - interface Issuance { - asset_id: string; - is_reissuance: string; - asset_blinding_nonce: string; - asset_entropy: string; - contract_hash: string; - assetamount?: number; - assetamountcommitment?: string; - tokenamount?: number; - tokenamountcommitment?: string; - } - - export interface Vout { - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_type: string; - scriptpubkey_address: string; - value: number; - // Elements - valuecommitment?: number; - asset?: string; - pegout?: Pegout; - } - - interface Pegout { - genesis_hash: string; - scriptpubkey: string; - scriptpubkey_asm: string; - scriptpubkey_address: string; - } - - export interface Status { - confirmed: boolean; - block_height?: number; - block_hash?: string; - block_time?: number; - } - - export interface Block { - id: string; - height: number; - version: number; - timestamp: number; - bits: number; - nonce: number; - difficulty: number; - merkle_root: string; - tx_count: number; - size: number; - weight: number; - previousblockhash: string; - } - - export interface Address { - address: string; - chain_stats: ChainStats; - mempool_stats: MempoolStats; - electrum?: boolean; - } - - export interface ChainStats { - funded_txo_count: number; - funded_txo_sum: number; - spent_txo_count: number; - spent_txo_sum: number; - tx_count: number; - } - - export interface MempoolStats { - funded_txo_count: number; - funded_txo_sum: number; - spent_txo_count: number; - spent_txo_sum: number; - tx_count: number; - } - - export interface Outspend { - spent: boolean; - txid?: string; - vin?: number; - status?: Status; - } - - export interface Asset { - asset_id: string; - issuance_txin: IssuanceTxin; - issuance_prevout: IssuancePrevout; - reissuance_token: string; - contract_hash: string; - status: Status; - chain_stats: AssetStats; - mempool_stats: AssetStats; - } - - export interface AssetExtended extends Asset { - name: string; - ticker: string; - precision: number; - entity: Entity; - version: number; - issuer_pubkey: string; - } - - export interface Entity { - domain: string; - } - - interface IssuanceTxin { - txid: string; - vin: number; - } - - interface IssuancePrevout { - txid: string; - vout: number; - } - - interface AssetStats { - tx_count: number; - issuance_count: number; - issued_amount: number; - burned_amount: number; - has_blinded_issuances: boolean; - reissuance_tokens: number; - burned_reissuance_tokens: number; - peg_in_count: number; - peg_in_amount: number; - peg_out_count: number; - peg_out_amount: number; - burn_count: number; - } - -} diff --git a/lightning-backend/src/api/bitcoin/esplora-api.ts b/lightning-backend/src/api/bitcoin/esplora-api.ts deleted file mode 100644 index 6ed48a0f8..000000000 --- a/lightning-backend/src/api/bitcoin/esplora-api.ts +++ /dev/null @@ -1,84 +0,0 @@ -import config from '../../config'; -import axios, { AxiosRequestConfig } from 'axios'; -import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; -import { IEsploraApi } from './esplora-api.interface'; - -class ElectrsApi implements AbstractBitcoinApi { - axiosConfig: AxiosRequestConfig = { - timeout: 10000, - }; - - constructor() { } - - $getRawMempool(): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig) - .then((response) => response.data); - } - - $getRawTransaction(txId: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) - .then((response) => response.data); - } - - $getBlockHeightTip(): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) - .then((response) => response.data); - } - - $getTxIdsForBlock(hash: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) - .then((response) => response.data); - } - - $getBlockHash(height: number): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig) - .then((response) => response.data); - } - - $getBlockHeader(hash: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig) - .then((response) => response.data); - } - - $getBlock(hash: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig) - .then((response) => response.data); - } - - $getAddress(address: string): Promise { - throw new Error('Method getAddress not implemented.'); - } - - $getAddressTransactions(address: string, txId?: string): Promise { - throw new Error('Method getAddressTransactions not implemented.'); - } - - $getAddressPrefix(prefix: string): string[] { - throw new Error('Method not implemented.'); - } - - $sendRawTransaction(rawTransaction: string): Promise { - throw new Error('Method not implemented.'); - } - - $getOutspend(txId: string, vout: number): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) - .then((response) => response.data); - } - - $getOutspends(txId: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) - .then((response) => response.data); - } - - async $getBatchedOutspends(txId: string[]): Promise { - const outspends: IEsploraApi.Outspend[][] = []; - for (const tx of txId) { - const outspend = await this.$getOutspends(tx); - outspends.push(outspend); - } - return outspends; - } -} - -export default ElectrsApi; diff --git a/lightning-backend/src/api/bitcoin/rpc-api/commands.ts b/lightning-backend/src/api/bitcoin/rpc-api/commands.ts deleted file mode 100644 index ea9bd7bf0..000000000 --- a/lightning-backend/src/api/bitcoin/rpc-api/commands.ts +++ /dev/null @@ -1,92 +0,0 @@ -module.exports = { - addMultiSigAddress: 'addmultisigaddress', - addNode: 'addnode', // bitcoind v0.8.0+ - backupWallet: 'backupwallet', - createMultiSig: 'createmultisig', - createRawTransaction: 'createrawtransaction', // bitcoind v0.7.0+ - decodeRawTransaction: 'decoderawtransaction', // bitcoind v0.7.0+ - decodeScript: 'decodescript', - dumpPrivKey: 'dumpprivkey', - dumpWallet: 'dumpwallet', // bitcoind v0.9.0+ - encryptWallet: 'encryptwallet', - estimateFee: 'estimatefee', // bitcoind v0.10.0x - estimatePriority: 'estimatepriority', // bitcoind v0.10.0+ - generate: 'generate', // bitcoind v0.11.0+ - getAccount: 'getaccount', - getAccountAddress: 'getaccountaddress', - getAddedNodeInfo: 'getaddednodeinfo', // bitcoind v0.8.0+ - getAddressesByAccount: 'getaddressesbyaccount', - getBalance: 'getbalance', - getBestBlockHash: 'getbestblockhash', // bitcoind v0.9.0+ - getBlock: 'getblock', - getBlockStats: 'getblockstats', - getBlockFilter: 'getblockfilter', - getBlockchainInfo: 'getblockchaininfo', // bitcoind v0.9.2+ - getBlockCount: 'getblockcount', - getBlockHash: 'getblockhash', - getBlockHeader: 'getblockheader', - getBlockTemplate: 'getblocktemplate', // bitcoind v0.7.0+ - getChainTips: 'getchaintips', // bitcoind v0.10.0+ - getChainTxStats: 'getchaintxstats', - getConnectionCount: 'getconnectioncount', - getDifficulty: 'getdifficulty', - getGenerate: 'getgenerate', - getInfo: 'getinfo', - getMempoolAncestors: 'getmempoolancestors', - getMempoolDescendants: 'getmempooldescendants', - getMempoolEntry: 'getmempoolentry', - getMempoolInfo: 'getmempoolinfo', // bitcoind v0.10+ - getMiningInfo: 'getmininginfo', - getNetTotals: 'getnettotals', - getNetworkInfo: 'getnetworkinfo', // bitcoind v0.9.2+ - getNetworkHashPs: 'getnetworkhashps', // bitcoind v0.9.0+ - getNewAddress: 'getnewaddress', - getPeerInfo: 'getpeerinfo', // bitcoind v0.7.0+ - getRawChangeAddress: 'getrawchangeaddress', // bitcoin v0.9+ - getRawMemPool: 'getrawmempool', // bitcoind v0.7.0+ - getRawTransaction: 'getrawtransaction', // bitcoind v0.7.0+ - getReceivedByAccount: 'getreceivedbyaccount', - getReceivedByAddress: 'getreceivedbyaddress', - getTransaction: 'gettransaction', - getTxOut: 'gettxout', // bitcoind v0.7.0+ - getTxOutProof: 'gettxoutproof', // bitcoind v0.11.0+ - getTxOutSetInfo: 'gettxoutsetinfo', // bitcoind v0.7.0+ - getUnconfirmedBalance: 'getunconfirmedbalance', // bitcoind v0.9.0+ - getWalletInfo: 'getwalletinfo', // bitcoind v0.9.2+ - help: 'help', - importAddress: 'importaddress', // bitcoind v0.10.0+ - importPrivKey: 'importprivkey', - importWallet: 'importwallet', // bitcoind v0.9.0+ - keypoolRefill: 'keypoolrefill', - keyPoolRefill: 'keypoolrefill', - listAccounts: 'listaccounts', - listAddressGroupings: 'listaddressgroupings', // bitcoind v0.7.0+ - listLockUnspent: 'listlockunspent', // bitcoind v0.8.0+ - listReceivedByAccount: 'listreceivedbyaccount', - listReceivedByAddress: 'listreceivedbyaddress', - listSinceBlock: 'listsinceblock', - listTransactions: 'listtransactions', - listUnspent: 'listunspent', // bitcoind v0.7.0+ - lockUnspent: 'lockunspent', // bitcoind v0.8.0+ - move: 'move', - ping: 'ping', // bitcoind v0.9.0+ - prioritiseTransaction: 'prioritisetransaction', // bitcoind v0.10.0+ - sendFrom: 'sendfrom', - sendMany: 'sendmany', - sendRawTransaction: 'sendrawtransaction', // bitcoind v0.7.0+ - sendToAddress: 'sendtoaddress', - setAccount: 'setaccount', - setGenerate: 'setgenerate', - setTxFee: 'settxfee', - signMessage: 'signmessage', - signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ - stop: 'stop', - submitBlock: 'submitblock', // bitcoind v0.7.0+ - validateAddress: 'validateaddress', - verifyChain: 'verifychain', // bitcoind v0.9.0+ - verifyMessage: 'verifymessage', - verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+ - walletLock: 'walletlock', - walletPassphrase: 'walletpassphrase', - walletPassphraseChange: 'walletpassphrasechange' -} diff --git a/lightning-backend/src/api/bitcoin/rpc-api/index.ts b/lightning-backend/src/api/bitcoin/rpc-api/index.ts deleted file mode 100644 index 131e1a048..000000000 --- a/lightning-backend/src/api/bitcoin/rpc-api/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -var commands = require('./commands') -var rpc = require('./jsonrpc') - -// ===----------------------------------------------------------------------===// -// JsonRPC -// ===----------------------------------------------------------------------===// -function Client (opts) { - // @ts-ignore - this.rpc = new rpc.JsonRPC(opts) -} - -// ===----------------------------------------------------------------------===// -// cmd -// ===----------------------------------------------------------------------===// -Client.prototype.cmd = function () { - var args = [].slice.call(arguments) - var cmd = args.shift() - - callRpc(cmd, args, this.rpc) -} - -// ===----------------------------------------------------------------------===// -// callRpc -// ===----------------------------------------------------------------------===// -function callRpc (cmd, args, rpc) { - var fn = args[args.length - 1] - - // If the last argument is a callback, pop it from the args list - if (typeof fn === 'function') { - args.pop() - } else { - fn = function () {} - } - - return rpc.call(cmd, args, function () { - var args = [].slice.call(arguments) - // @ts-ignore - args.unshift(null) - // @ts-ignore - fn.apply(this, args) - }, function (err) { - fn(err) - }) -} - -// ===----------------------------------------------------------------------===// -// Initialize wrappers -// ===----------------------------------------------------------------------===// -(function () { - for (var protoFn in commands) { - (function (protoFn) { - Client.prototype[protoFn] = function () { - var args = [].slice.call(arguments) - return callRpc(commands[protoFn], args, this.rpc) - } - })(protoFn) - } -})() - -// Export! -module.exports.Client = Client; diff --git a/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts b/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts deleted file mode 100644 index 4f7a38baa..000000000 --- a/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts +++ /dev/null @@ -1,162 +0,0 @@ -var http = require('http') -var https = require('https') - -var JsonRPC = function (opts) { - // @ts-ignore - this.opts = opts || {} - // @ts-ignore - this.http = this.opts.ssl ? https : http -} - -JsonRPC.prototype.call = function (method, params) { - return new Promise((resolve, reject) => { - var time = Date.now() - var requestJSON - - if (Array.isArray(method)) { - // multiple rpc batch call - requestJSON = [] - method.forEach(function (batchCall, i) { - requestJSON.push({ - id: time + '-' + i, - method: batchCall.method, - params: batchCall.params - }) - }) - } else { - // single rpc call - requestJSON = { - id: time, - method: method, - params: params - } - } - - // First we encode the request into JSON - requestJSON = JSON.stringify(requestJSON) - - // prepare request options - var requestOptions = { - host: this.opts.host || 'localhost', - port: this.opts.port || 8332, - method: 'POST', - path: '/', - headers: { - 'Host': this.opts.host || 'localhost', - 'Content-Length': requestJSON.length - }, - agent: false, - rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false - } - - if (this.opts.ssl && this.opts.sslCa) { - // @ts-ignore - requestOptions.ca = this.opts.sslCa - } - - // use HTTP auth if user and password set - if (this.opts.user && this.opts.pass) { - // @ts-ignore - requestOptions.auth = this.opts.user + ':' + this.opts.pass - } - - // Now we'll make a request to the server - var cbCalled = false - var request = this.http.request(requestOptions) - - // start request timeout timer - var reqTimeout = setTimeout(function () { - if (cbCalled) return - cbCalled = true - request.abort() - var err = new Error('ETIMEDOUT') - // @ts-ignore - err.code = 'ETIMEDOUT' - reject(err) - }, this.opts.timeout || 30000) - - // set additional timeout on socket in case of remote freeze after sending headers - request.setTimeout(this.opts.timeout || 30000, function () { - if (cbCalled) return - cbCalled = true - request.abort() - var err = new Error('ESOCKETTIMEDOUT') - // @ts-ignore - err.code = 'ESOCKETTIMEDOUT' - reject(err) - }) - - request.on('error', function (err) { - if (cbCalled) return - cbCalled = true - clearTimeout(reqTimeout) - reject(err) - }) - - request.on('response', function (response) { - clearTimeout(reqTimeout) - - // We need to buffer the response chunks in a nonblocking way. - var buffer = '' - response.on('data', function (chunk) { - buffer = buffer + chunk - }) - // When all the responses are finished, we decode the JSON and - // depending on whether it's got a result or an error, we call - // emitSuccess or emitError on the promise. - response.on('end', function () { - var err - - if (cbCalled) return - cbCalled = true - - try { - var decoded = JSON.parse(buffer) - } catch (e) { - if (response.statusCode !== 200) { - err = new Error('Invalid params, response status code: ' + response.statusCode) - err.code = -32602 - reject(err) - } else { - err = new Error('Problem parsing JSON response from server') - err.code = -32603 - reject(err) - } - return - } - - if (!Array.isArray(decoded)) { - decoded = [decoded] - } - - // iterate over each response, normally there will be just one - // unless a batch rpc call response is being processed - decoded.forEach(function (decodedResponse, i) { - if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) { - if (reject) { - err = new Error(decodedResponse.error.message || '') - if (decodedResponse.error.code) { - err.code = decodedResponse.error.code - } - reject(err) - } - } else if (decodedResponse.hasOwnProperty('result')) { - // @ts-ignore - resolve(decodedResponse.result, response.headers) - } else { - if (reject) { - err = new Error(decodedResponse.error.message || '') - if (decodedResponse.error.code) { - err.code = decodedResponse.error.code - } - reject(err) - } - } - }) - }) - }) - request.end(requestJSON); - }); -} - -module.exports.JsonRPC = JsonRPC diff --git a/lightning-backend/src/api/explorer/statistics.api.ts b/lightning-backend/src/api/explorer/statistics.api.ts deleted file mode 100644 index 620e76fef..000000000 --- a/lightning-backend/src/api/explorer/statistics.api.ts +++ /dev/null @@ -1,17 +0,0 @@ -import logger from '../../logger'; -import DB from '../../database'; - -class StatisticsApi { - public async $getStatistics(): Promise { - try { - const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM statistics ORDER BY id DESC`; - const [rows]: any = await DB.query(query); - return rows; - } catch (e) { - logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e)); - throw e; - } - } -} - -export default new StatisticsApi(); diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts deleted file mode 100644 index df821f6fb..000000000 --- a/lightning-backend/src/config.ts +++ /dev/null @@ -1,110 +0,0 @@ -const configFile = require('../mempool-config.json'); - -interface IConfig { - MEMPOOL: { - NETWORK: 'mainnet' | 'testnet' | 'signet'; - BACKEND: 'lnd' | 'cln' | 'ldk'; - HTTP_PORT: number; - API_URL_PREFIX: string; - STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; - }; - ESPLORA: { - REST_API_URL: string; - }; - SYSLOG: { - ENABLED: boolean; - HOST: string; - PORT: number; - MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; - FACILITY: string; - }; - LN_NODE_AUTH: { - TLS_CERT_PATH: string; - MACAROON_PATH: string; - SOCKET: string; - }; - CORE_RPC: { - HOST: string; - PORT: number; - USERNAME: string; - PASSWORD: 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, - 'API_URL_PREFIX': '/api/v1/', - 'STDOUT_LOG_MIN_PRIORITY': 'debug', - }, - 'ESPLORA': { - 'REST_API_URL': 'http://127.0.0.1:3000', - }, - 'SYSLOG': { - 'ENABLED': true, - 'HOST': '127.0.0.1', - 'PORT': 514, - 'MIN_PRIORITY': 'info', - 'FACILITY': 'local7' - }, - 'LN_NODE_AUTH': { - 'TLS_CERT_PATH': '', - 'MACAROON_PATH': '', - 'SOCKET': 'localhost:10009', - }, - 'CORE_RPC': { - 'HOST': '127.0.0.1', - 'PORT': 8332, - 'USERNAME': 'mempool', - 'PASSWORD': 'mempool' - }, - 'DATABASE': { - 'HOST': '127.0.0.1', - 'SOCKET': '', - 'PORT': 3306, - 'DATABASE': 'mempool', - 'USERNAME': 'mempool', - 'PASSWORD': 'mempool' - }, -}; - -class Config implements IConfig { - MEMPOOL: IConfig['MEMPOOL']; - ESPLORA: IConfig['ESPLORA']; - SYSLOG: IConfig['SYSLOG']; - LN_NODE_AUTH: IConfig['LN_NODE_AUTH']; - CORE_RPC: IConfig['CORE_RPC']; - DATABASE: IConfig['DATABASE']; - - constructor() { - const configs = this.merge(configFile, defaults); - this.MEMPOOL = configs.MEMPOOL; - this.ESPLORA = configs.ESPLORA; - this.SYSLOG = configs.SYSLOG; - this.LN_NODE_AUTH = configs.LN_NODE_AUTH; - this.CORE_RPC = configs.CORE_RPC; - 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-migration.ts b/lightning-backend/src/database-migration.ts deleted file mode 100644 index ebf149ac9..000000000 --- a/lightning-backend/src/database-migration.ts +++ /dev/null @@ -1,260 +0,0 @@ -import config from './config'; -import DB from './database'; -import logger from './logger'; - -const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); - -class DatabaseMigration { - private static currentVersion = 1; - private queryTimeout = 120000; - - constructor() { } - /** - * Entry point - */ - public async $initializeOrMigrateDatabase(): Promise { - logger.debug('MIGRATIONS: Running migrations'); - - await this.$printDatabaseVersion(); - - // First of all, if the `state` database does not exist, create it so we can track migration version - if (!await this.$checkIfTableExists('state')) { - logger.debug('MIGRATIONS: `state` table does not exist. Creating it.'); - try { - await this.$createMigrationStateTable(); - } catch (e) { - logger.err('MIGRATIONS: Unable to create `state` table, aborting in 10 seconds. ' + e); - await sleep(10000); - process.exit(-1); - } - logger.debug('MIGRATIONS: `state` table initialized.'); - } - - let databaseSchemaVersion = 0; - try { - databaseSchemaVersion = await this.$getSchemaVersionFromDatabase(); - } catch (e) { - logger.err('MIGRATIONS: Unable to get current database migration version, aborting in 10 seconds. ' + e); - await sleep(10000); - process.exit(-1); - } - - logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion); - logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion); - if (databaseSchemaVersion >= DatabaseMigration.currentVersion) { - logger.debug('MIGRATIONS: Nothing to do.'); - return; - } - - // Now, create missing tables. Those queries cannot be wrapped into a transaction unfortunately - try { - await this.$createMissingTablesAndIndexes(databaseSchemaVersion); - } catch (e) { - logger.err('MIGRATIONS: Unable to create required tables, aborting in 10 seconds. ' + e); - await sleep(10000); - process.exit(-1); - } - - if (DatabaseMigration.currentVersion > databaseSchemaVersion) { - logger.notice('MIGRATIONS: Upgrading datababse schema'); - try { - await this.$migrateTableSchemaFromVersion(databaseSchemaVersion); - logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`); - } catch (e) { - logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e); - } - } - - return; - } - - /** - * Create all missing tables - */ - private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) { - try { - await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); - await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); - await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); - await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); - } catch (e) { - throw e; - } - } - - /** - * Small query execution wrapper to log all executed queries - */ - private async $executeQuery(query: string, silent: boolean = false): Promise { - if (!silent) { - logger.debug('MIGRATIONS: Execute query:\n' + query); - } - return DB.query({ sql: query, timeout: this.queryTimeout }); - } - - /** - * Check if 'table' exists in the database - */ - private async $checkIfTableExists(table: string): Promise { - const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`; - const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout }); - return rows[0]['COUNT(*)'] === 1; - } - - /** - * Get current database version - */ - private async $getSchemaVersionFromDatabase(): Promise { - const query = `SELECT number FROM state WHERE name = 'schema_version';`; - const [rows] = await this.$executeQuery(query, true); - return rows[0]['number']; - } - - /** - * Create the `state` table - */ - private async $createMigrationStateTable(): Promise { - try { - const query = `CREATE TABLE IF NOT EXISTS state ( - name varchar(25) NOT NULL, - number int(11) NULL, - string varchar(100) NULL, - CONSTRAINT name_unique UNIQUE (name) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - await this.$executeQuery(query); - - // Set initial values - await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`); - await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`); - } catch (e) { - throw e; - } - } - - /** - * We actually execute the migrations queries here - */ - private async $migrateTableSchemaFromVersion(version: number): Promise { - const transactionQueries: string[] = []; - for (const query of this.getMigrationQueriesFromVersion(version)) { - transactionQueries.push(query); - } - transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery()); - - try { - await this.$executeQuery('START TRANSACTION;'); - for (const query of transactionQueries) { - await this.$executeQuery(query); - } - await this.$executeQuery('COMMIT;'); - } catch (e) { - await this.$executeQuery('ROLLBACK;'); - throw e; - } - } - - /** - * Generate migration queries based on schema version - */ - private getMigrationQueriesFromVersion(version: number): string[] { - const queries: string[] = []; - return queries; - } - - /** - * Save the schema version in the database - */ - private getUpdateToLatestSchemaVersionQuery(): string { - return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`; - } - - /** - * Print current database version - */ - private async $printDatabaseVersion() { - try { - const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true); - logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`); - } catch (e) { - logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e); - } - } - - private getCreateStatisticsQuery(): string { - return `CREATE TABLE IF NOT EXISTS statistics ( - id int(11) NOT NULL AUTO_INCREMENT, - added datetime NOT NULL, - channel_count int(11) NOT NULL, - node_count int(11) NOT NULL, - total_capacity double unsigned NOT NULL, - PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - } - - private getCreateNodesQuery(): string { - return `CREATE TABLE IF NOT EXISTS nodes ( - public_key varchar(66) NOT NULL, - first_seen datetime NOT NULL, - updated_at datetime NOT NULL, - alias varchar(200) CHARACTER SET utf8mb4 NOT NULL, - color varchar(200) NOT NULL, - sockets text DEFAULT NULL, - PRIMARY KEY (public_key), - KEY alias (alias(10)) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - } - - private getCreateChannelsQuery(): string { - return `CREATE TABLE IF NOT EXISTS channels ( - id bigint(11) unsigned NOT NULL, - short_id varchar(15) NOT NULL DEFAULT '', - capacity bigint(20) unsigned NOT NULL, - transaction_id varchar(64) NOT NULL, - transaction_vout int(11) NOT NULL, - updated_at datetime DEFAULT NULL, - created datetime DEFAULT NULL, - status int(11) NOT NULL DEFAULT 0, - closing_transaction_id varchar(64) DEFAULT NULL, - closing_date datetime DEFAULT NULL, - closing_reason int(11) DEFAULT NULL, - node1_public_key varchar(66) NOT NULL, - node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, - node1_cltv_delta int(11) DEFAULT NULL, - node1_fee_rate bigint(11) DEFAULT NULL, - node1_is_disabled tinyint(1) DEFAULT NULL, - node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, - node1_min_htlc_mtokens bigint(20) DEFAULT NULL, - node1_updated_at datetime DEFAULT NULL, - node2_public_key varchar(66) NOT NULL, - node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL, - node2_cltv_delta int(11) DEFAULT NULL, - node2_fee_rate bigint(11) DEFAULT NULL, - node2_is_disabled tinyint(1) DEFAULT NULL, - node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL, - node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL, - node2_updated_at datetime DEFAULT NULL, - PRIMARY KEY (id), - KEY node1_public_key (node1_public_key), - KEY node2_public_key (node2_public_key), - KEY status (status), - KEY short_id (short_id), - KEY transaction_id (transaction_id), - KEY closing_transaction_id (closing_transaction_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - } - - private getCreateNodesStatsQuery(): string { - return `CREATE TABLE IF NOT EXISTS node_stats ( - id int(11) unsigned NOT NULL AUTO_INCREMENT, - public_key varchar(66) NOT NULL DEFAULT '', - added date NOT NULL, - capacity bigint(20) unsigned NOT NULL DEFAULT 0, - channels int(11) unsigned NOT NULL DEFAULT 0, - PRIMARY KEY (id), - UNIQUE KEY added (added,public_key), - KEY public_key (public_key) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; - } -} - -export default new DatabaseMigration(); diff --git a/lightning-backend/src/database.ts b/lightning-backend/src/database.ts deleted file mode 100644 index 3816154cd..000000000 --- a/lightning-backend/src/database.ts +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index fc7a95f2d..000000000 --- a/lightning-backend/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import DB from './database'; -import databaseMigration from './database-migration'; -import statsUpdater from './tasks/stats-updater.service'; -import nodeSyncService from './tasks/node-sync.service'; -import server from './server'; - -class LightningServer { - constructor() { - this.init(); - } - - async init() { - await DB.checkDbConnection(); - await databaseMigration.$initializeOrMigrateDatabase(); - - nodeSyncService.$startService(); - statsUpdater.$startService(); - - server.startServer(); - } -} - -const lightningServer = new LightningServer(); diff --git a/lightning-backend/src/logger.ts b/lightning-backend/src/logger.ts deleted file mode 100644 index 1e2c95ed1..000000000 --- a/lightning-backend/src/logger.ts +++ /dev/null @@ -1,145 +0,0 @@ -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(); diff --git a/lightning-backend/src/server.ts b/lightning-backend/src/server.ts deleted file mode 100644 index afee2e070..000000000 --- a/lightning-backend/src/server.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Express, Request, Response, NextFunction } from 'express'; -import * as express from 'express'; -import * as http from 'http'; -import logger from './logger'; -import config from './config'; -import generalRoutes from './api/explorer/general.routes'; -import nodesRoutes from './api/explorer/nodes.routes'; -import channelsRoutes from './api/explorer/channels.routes'; - -class Server { - private server: http.Server | undefined; - private app: Express = express(); - - public startServer() { - this.app - .use((req: Request, res: Response, next: NextFunction) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - next(); - }) - .use(express.urlencoded({ extended: true })) - .use(express.text()) - ; - - this.server = http.createServer(this.app); - - this.server.listen(config.MEMPOOL.HTTP_PORT, () => { - logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`); - }); - - this.initRoutes(); - } - - private initRoutes() { - generalRoutes.initRoutes(this.app); - nodesRoutes.initRoutes(this.app); - channelsRoutes.initRoutes(this.app); - } -} - -export default new Server(); diff --git a/lightning-backend/tsconfig.json b/lightning-backend/tsconfig.json deleted file mode 100644 index 8b4cbea2e..000000000 --- a/lightning-backend/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "esnext", - "lib": ["es2019", "dom"], - "strict": true, - "noImplicitAny": false, - "sourceMap": false, - "outDir": "dist", - "moduleResolution": "node", - "typeRoots": [ - "node_modules/@types" - ], - "allowSyntheticDefaultImports": true - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "dist/**" - ] -} \ No newline at end of file diff --git a/lightning-backend/tslint.json b/lightning-backend/tslint.json deleted file mode 100644 index 945512322..000000000 --- a/lightning-backend/tslint.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "rules": { - "arrow-return-shorthand": true, - "callable-types": true, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": true, - "deprecation": { - "severity": "warn" - }, - "eofline": true, - "forin": false, - "import-blacklist": [ - true, - "rxjs", - "rxjs/Rx" - ], - "import-spacing": true, - "indent": [ - true, - "spaces" - ], - "interface-over-type-literal": true, - "label-position": true, - "max-line-length": [ - true, - 140 - ], - "member-access": false, - "member-ordering": [ - true, - { - "order": [ - "static-field", - "instance-field", - "static-method", - "instance-method" - ] - } - ], - "no-arg": true, - "no-bitwise": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, - "no-empty": false, - "no-empty-interface": true, - "no-eval": true, - "no-inferrable-types": false, - "no-misused-new": true, - "no-non-null-assertion": true, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-string-throw": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unnecessary-initializer": true, - "no-unused-expression": true, - "no-use-before-declare": true, - "no-var-keyword": true, - "object-literal-sort-keys": false, - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-else", - "check-whitespace" - ], - "prefer-const": true, - "quotemark": [ - true, - "single" - ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "unified-signatures": true, - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ], - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ], - "no-output-on-prefix": true, - "use-input-property-decorator": true, - "use-output-property-decorator": true, - "use-host-property-decorator": true, - "no-input-rename": true, - "no-output-rename": true, - "use-life-cycle-interface": true, - "use-pipe-transform-interface": true, - "component-class-suffix": true, - "directive-class-suffix": true - } -} From 71b304cafdea6f21f7464123dee2ba0c423f05d0 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 6 Jul 2022 13:55:21 +0200 Subject: [PATCH 39/51] Renaming config LND_NODE_AUTH to LND --- backend/mempool-config.sample.json | 2 +- backend/src/api/lightning/lnd/lnd-api.ts | 6 +++--- backend/src/config.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 9fe8ca36a..eea54ae46 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -70,7 +70,7 @@ "ENABLED": false, "BACKEND": "lnd" }, - "LND_NODE_AUTH": { + "LND": { "TLS_CERT_PATH": "tls.cert", "MACAROON_PATH": "admin.macaroon", "SOCKET": "localhost:10009" diff --git a/backend/src/api/lightning/lnd/lnd-api.ts b/backend/src/api/lightning/lnd/lnd-api.ts index 87c182a42..7d4e8c96e 100644 --- a/backend/src/api/lightning/lnd/lnd-api.ts +++ b/backend/src/api/lightning/lnd/lnd-api.ts @@ -12,13 +12,13 @@ class LndApi implements AbstractLightningApi { return; } try { - const tls = fs.readFileSync(config.LND_NODE_AUTH.TLS_CERT_PATH).toString('base64'); - const macaroon = fs.readFileSync(config.LND_NODE_AUTH.MACAROON_PATH).toString('base64'); + 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({ cert: tls, macaroon: macaroon, - socket: config.LND_NODE_AUTH.SOCKET, + socket: config.LND.SOCKET, }); this.lnd = lnd; diff --git a/backend/src/config.ts b/backend/src/config.ts index acc0b30a0..c9ec65e76 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -31,7 +31,7 @@ interface IConfig { ENABLED: boolean; BACKEND: 'lnd' | 'cln' | 'ldk'; }; - LND_NODE_AUTH: { + LND: { TLS_CERT_PATH: string; MACAROON_PATH: string; SOCKET: string; @@ -171,7 +171,7 @@ const defaults: IConfig = { 'ENABLED': false, 'BACKEND': 'lnd' }, - 'LND_NODE_AUTH': { + 'LND': { 'TLS_CERT_PATH': '', 'MACAROON_PATH': '', 'SOCKET': 'localhost:10009', @@ -209,7 +209,7 @@ class Config implements IConfig { STATISTICS: IConfig['STATISTICS']; BISQ: IConfig['BISQ']; LIGHTNING: IConfig['LIGHTNING']; - LND_NODE_AUTH: IConfig['LND_NODE_AUTH']; + LND: IConfig['LND']; SOCKS5PROXY: IConfig['SOCKS5PROXY']; PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; @@ -226,7 +226,7 @@ class Config implements IConfig { this.STATISTICS = configs.STATISTICS; this.BISQ = configs.BISQ; this.LIGHTNING = configs.LIGHTNING; - this.LND_NODE_AUTH = configs.LND_NODE_AUTH; + this.LND = configs.LND; this.SOCKS5PROXY = configs.SOCKS5PROXY; this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; From 17a6b7fefdb9a939ca3090aba9225524af1ba961 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 6 Jul 2022 14:20:11 +0200 Subject: [PATCH 40/51] Restoring the TV button --- .../src/app/components/master-page/master-page.component.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index bef1dba8c..8bbe2c6ca 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -44,6 +44,9 @@ + From 89c4023ddf2640c70de9d8d1f5b1699e472c01d1 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 6 Jul 2022 15:35:02 +0200 Subject: [PATCH 41/51] Fix for search box tests --- frontend/cypress/e2e/mainnet/mainnet.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 400e09605..14f2c88de 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -121,20 +121,20 @@ describe('Mainnet', () => { cy.visit('/'); cy.get('.search-box-container > .form-control').type('1wiz').then(() => { cy.wait('@search-1wiz'); - cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 10); + cy.get('app-search-results button.dropdown-item').should('have.length', 10); }); cy.get('.search-box-container > .form-control').type('S').then(() => { cy.wait('@search-1wizS'); - cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 5); + cy.get('app-search-results button.dropdown-item').should('have.length', 5); }); cy.get('.search-box-container > .form-control').type('A').then(() => { cy.wait('@search-1wizSA'); - cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1) + cy.get('app-search-results button.dropdown-item').should('have.length', 1) }); - cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => { + cy.get('app-search-results button.dropdown-item.active').click().then(() => { cy.url().should('include', '/address/1wizSAYSbuyXbt9d8JV8ytm5acqq2TorC'); cy.waitForSkeletonGone(); cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); @@ -145,8 +145,8 @@ describe('Mainnet', () => { it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => { cy.visit('/'); cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { - cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1); - cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => { + cy.get('app-search-results button.dropdown-item').should('have.length', 1); + cy.get('app-search-results button.dropdown-item.active').click().then(() => { cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e'); cy.waitForSkeletonGone(); cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); @@ -159,8 +159,8 @@ describe('Mainnet', () => { it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => { cy.visit('/'); cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { - cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1); - cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => { + cy.get('app-search-results button.dropdown-item').should('have.length', 1); + cy.get('app-search-results button.dropdown-item.active').click().then(() => { cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy'); cy.waitForSkeletonGone(); cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); From 850060cc07da4d6e00390ada229c102d200aa0ff Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 6 Jul 2022 16:07:21 +0200 Subject: [PATCH 42/51] Search result click fix. --- .../src/app/components/search-form/search-form.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index d34659f8f..55279ca05 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -111,7 +111,7 @@ export class SearchFormComponent implements OnInit { selectedResult(result: any) { if (typeof result === 'string') { - this.search(); + this.search(result); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); } else if (result.short_id) { @@ -119,8 +119,8 @@ export class SearchFormComponent implements OnInit { } } - search() { - const searchText = this.searchForm.value.searchText.trim(); + search(result?: string) { + const searchText = result || this.searchForm.value.searchText.trim(); if (searchText) { this.isSearching = true; if (this.regexAddress.test(searchText)) { From 1f2254681ae18ac7a75d0a3269d3ad0e21db4034 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 6 Jul 2022 16:21:14 +0200 Subject: [PATCH 43/51] Search result fix when Lightning not enabled --- .../src/app/components/search-form/search-form.component.ts | 6 ++++++ .../search-results/search-results.component.html | 2 +- .../search-form/search-results/search-results.component.ts | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 55279ca05..9ed40700a 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -80,6 +80,12 @@ export class SearchFormComponent implements OnInit { } ]); } + if (!this.stateService.env.LIGHTNING) { + return zip( + this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), + [{ nodes: [], channels: [] }] + ); + } return zip( this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), this.apiService.lightningSearch$(text).pipe(catchError(() => of({ diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index 8c18b1565..f7193f261 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -1,6 +1,6 @@