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)); } }