From 9c8fd6431e7de902cb7944c94f92fe54299dab73 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 17 Aug 2022 21:20:11 +0200 Subject: [PATCH] Create node rankings page with 3 different rankings --- backend/src/api/explorer/nodes.api.ts | 48 +++++++++++ backend/src/api/explorer/nodes.routes.ts | 13 +++ backend/src/mempool.interfaces.ts | 11 +++ .../src/app/interfaces/node-api.interface.ts | 11 +++ .../app/lightning/lightning-api.service.ts | 8 +- .../lightning-dashboard.component.html | 4 +- .../src/app/lightning/lightning.module.ts | 7 ++ .../app/lightning/lightning.routing.module.ts | 5 ++ .../oldest-nodes/oldest-nodes.component.html | 71 ++++++++++++++++ .../oldest-nodes/oldest-nodes.component.scss | 84 +++++++++++++++++++ .../oldest-nodes/oldest-nodes.component.ts | 36 ++++++++ .../nodes-rankings-dashboard.component.html | 47 +++++++++++ .../nodes-rankings-dashboard.component.scss | 33 ++++++++ .../nodes-rankings-dashboard.component.ts | 25 ++++++ 14 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html create mode 100644 frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss create mode 100644 frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts create mode 100644 frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html create mode 100644 frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss create mode 100644 frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index c0a891a0d..339364374 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -209,6 +209,54 @@ class NodesApi { } } + public async $getOldestNodes(full: boolean): Promise { + try { + let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); + const latestDate = rows[0].maxAdded; + + let query: string; + if (full === false) { + query = ` + SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, + node_stats.channels + FROM node_stats + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY first_seen + LIMIT 100; + `; + + [rows] = await DB.query(query); + } else { + query = ` + SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias, + CAST(COALESCE(node_stats.channels, 0) as INT) as channels, + CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, + UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, + geo_names_city.names as city, geo_names_country.names as country + FROM node_stats + RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key + LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' + LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY first_seen + LIMIT 100 + `; + + [rows] = await DB.query(query); + for (let i = 0; i < rows.length; ++i) { + rows[i].country = JSON.parse(rows[i].country); + rows[i].city = JSON.parse(rows[i].city); + } + } + + return rows; + } catch (e) { + logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $searchNodeByPublicKeyOrAlias(search: string) { try { const searchStripped = search.replace('%', '') + '%'; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index aaafe679a..482f01f0f 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -17,6 +17,7 @@ class NodesRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings', this.$getNodesRanking) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/capacity', this.$getTopNodesByCapacity) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/channels', this.$getTopNodesByChannels) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes) .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) ; @@ -99,6 +100,18 @@ class NodesRoutes { } } + private async $getOldestNodes(req: Request, res: Response): Promise { + try { + const topCapacityNodes = await nodesApi.$getOldestNodes(true); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(topCapacityNodes); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getISPRanking(req: Request, res: Response): Promise { try { const groupBy = req.query.groupBy as string; diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index d4f904772..d72b13576 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -277,4 +277,15 @@ export interface ITopNodesPerCapacity { export interface INodesRanking { topByCapacity: ITopNodesPerCapacity[]; topByChannels: ITopNodesPerChannels[]; +} + +export interface IOldestNodes { + publicKey: string, + alias: string, + firstSeen: number, + channels?: number, + capacity: number, + updatedAt?: number, + city?: any, + country?: any, } \ No newline at end of file diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 2d1ff8f43..838208cc3 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -177,4 +177,15 @@ export interface ITopNodesPerCapacity { export interface INodesRanking { topByCapacity: ITopNodesPerCapacity[]; topByChannels: ITopNodesPerChannels[]; +} + +export interface IOldestNodes { + publicKey: string, + alias: string, + firstSeen: number, + channels?: number, + capacity: number, + updatedAt?: number, + city?: any, + country?: any, } \ No newline at end of file diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 1aacf7b89..f14b0b382 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { StateService } from '../services/state.service'; -import { INodesRanking, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; +import { INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @Injectable({ providedIn: 'root' @@ -75,4 +75,10 @@ export class LightningApiService { this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels' ); } + + getOldestNodes$(): Observable { + return this.httpClient.get( + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age' + ); + } } 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 3aaeed3dc..fe678a2fc 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -42,6 +42,7 @@ +
@@ -53,6 +54,7 @@
+
@@ -61,12 +63,12 @@   -
+
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 48d54c49c..7ca02b2ba 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -27,6 +27,9 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component'; import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component'; import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component'; +import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; +import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; + @NgModule({ declarations: [ LightningDashboardComponent, @@ -51,6 +54,8 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca NodesRanking, TopNodesPerChannels, TopNodesPerCapacity, + OldestNodes, + NodesRankingsDashboard, ], imports: [ CommonModule, @@ -82,6 +87,8 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca NodesRanking, TopNodesPerChannels, TopNodesPerCapacity, + OldestNodes, + NodesRankingsDashboard, ], providers: [ LightningApiService, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 9e9405234..56734ea7b 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -7,6 +7,7 @@ import { ChannelComponent } from './channel/channel.component'; import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component'; import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; import { NodesRanking } from './nodes-ranking/nodes-ranking.component'; +import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component'; const routes: Routes = [ { @@ -33,6 +34,10 @@ const routes: Routes = [ path: 'nodes/isp/:isp', component: NodesPerISP, }, + { + path: 'nodes/rankings', + component: NodesRankingsDashboard, + }, { path: 'nodes/top-capacity', component: NodesRanking, diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html new file mode 100644 index 000000000..ef18f9b72 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html @@ -0,0 +1,71 @@ +
+

+ Top 100 nodes by capacity +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliasFirst seenCapacityChannelsLast updateLocation
+ {{ i + 1 }} + + {{ node.alias }} + + ‎{{ node.firstSeen * 1000 | date: 'yyyy-MM-dd' }} + + + + {{ node.channels | number }} + + + + {{ node?.city?.en ?? '-' }} +
+ + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss new file mode 100644 index 000000000..f69a3ba67 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss @@ -0,0 +1,84 @@ +.container-xl { + max-width: 1400px; + padding-bottom: 100px; + @media (min-width: 767.98px) { + padding-left: 50px; + padding-right: 50px; + } +} + +.table td, .table th { + padding: 0.5rem; +} + +.full .rank { + width: 5%; +} +.widget .rank { + @media (min-width: 767.98px) { + width: 13%; + } + @media (max-width: 767.98px) { + padding-left: 0px; + padding-right: 0px; + } +} + +.full .alias { + width: 10%; + overflow: hidden; + text-overflow: ellipsis; + max-width: 350px; + @media (max-width: 767.98px) { + max-width: 175px; + } +} +.widget .alias { + width: 50%; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; + @media (max-width: 767.98px) { + max-width: 170px; + } +} + +.full .capacity { + width: 10%; +} +.widget .capacity { + width: 32%; + @media (max-width: 767.98px) { + padding-left: 0px; + padding-right: 0px; + } +} + +.full .channels { + width: 15%; + padding-right: 50px; + @media (max-width: 767.98px) { + display: none; + } +} + +.full .timestamp-first { + width: 50%; + @media (max-width: 767.98px) { + display: none; + } +} + +.full .timestamp-update { + width: 15%; + @media (max-width: 767.98px) { + display: none; + } +} + +.full .location { + width: 10%; + @media (max-width: 767.98px) { + display: none; + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts new file mode 100644 index 000000000..23f248b0e --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { IOldestNodes } from '../../../interfaces/node-api.interface'; +import { LightningApiService } from '../../lightning-api.service'; + +@Component({ + selector: 'app-oldest-nodes', + templateUrl: './oldest-nodes.component.html', + styleUrls: ['./oldest-nodes.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OldestNodes implements OnInit { + @Input() widget: boolean = false; + + oldestNodes$: Observable; + skeletonRows: number[] = []; + + constructor(private apiService: LightningApiService) {} + + ngOnInit(): void { + for (let i = 1; i <= (this.widget ? 10 : 100); ++i) { + this.skeletonRows.push(i); + } + + if (this.widget === false) { + this.oldestNodes$ = this.apiService.getOldestNodes$(); + } else { + this.oldestNodes$ = this.apiService.getOldestNodes$().pipe( + map((nodes: IOldestNodes[]) => { + return nodes.slice(0, 10); + }) + ); + } + } + +} diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html new file mode 100644 index 000000000..0359244f3 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html @@ -0,0 +1,47 @@ + \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss new file mode 100644 index 000000000..28e80d451 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss @@ -0,0 +1,33 @@ +.main { + max-width: 90%; +} + +.col { + padding-bottom: 20px; + padding-left: 10px; + padding-right: 10px; +} + +.card { + background-color: #1d1f31; +} + +.card-title { + font-size: 1rem; + color: #4a68b9; +} +.card-title > a { + color: #4a68b9; +} + +.card-text { + font-size: 22px; +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + text-align: center; + display: block; + margin-bottom: 10px; + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts new file mode 100644 index 000000000..4b39d8467 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable, share } from 'rxjs'; +import { INodesRanking } from 'src/app/interfaces/node-api.interface'; +import { SeoService } from 'src/app/services/seo.service'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-nodes-rankings-dashboard', + templateUrl: './nodes-rankings-dashboard.component.html', + styleUrls: ['./nodes-rankings-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodesRankingsDashboard implements OnInit { + nodesRanking$: Observable; + + constructor( + private lightningApiService: LightningApiService, + private seoService: SeoService, + ) {} + + ngOnInit(): void { + this.seoService.setTitle($localize`Top lightning nodes`); + this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); + } +}