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,