diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index c7b98f3a8..c0a891a0d 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -2,7 +2,7 @@ import logger from '../../logger'; import DB from '../../database'; import { ResultSetHeader } from 'mysql2'; import { ILightningApi } from '../lightning/lightning-api.interface'; -import { TopNodesPerCapacity, TopNodesPerChannels } from '../../mempool.interfaces'; +import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces'; class NodesApi { public async $getNode(public_key: string): Promise { @@ -113,7 +113,7 @@ class NodesApi { } } - public async $getTopCapacityNodes(full: boolean): Promise { + public async $getTopCapacityNodes(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; @@ -161,21 +161,46 @@ class NodesApi { } } - public async $getTopChannelsNodes(): Promise { + public async $getTopChannelsNodes(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; - const 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 channels DESC - LIMIT 100; - `; - [rows] = await DB.query(query); + 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 channels DESC + 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 channels DESC + 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) { diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index a490d946b..aaafe679a 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -16,6 +16,7 @@ class NodesRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries) .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/:public_key/statistics', this.$getHistoricalNodeStats) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) ; @@ -61,7 +62,7 @@ class NodesRoutes { private async $getNodesRanking(req: Request, res: Response): Promise { try { const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false); - const topChannelsNodes = await nodesApi.$getTopChannelsNodes(); + const topChannelsNodes = await nodesApi.$getTopChannelsNodes(false); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); @@ -86,6 +87,18 @@ class NodesRoutes { } } + private async $getTopNodesByChannels(req: Request, res: Response): Promise { + try { + const topCapacityNodes = await nodesApi.$getTopChannelsNodes(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 1067f9b6d..d4f904772 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -252,13 +252,18 @@ export interface RewardStats { totalTx: number; } -export interface TopNodesPerChannels { - public_key: string, +export interface ITopNodesPerChannels { + publicKey: string, alias: string, - channels: number, + channels?: number, + capacity: number, + firstSeen?: number, + updatedAt?: number, + city?: any, + country?: any, } -export interface TopNodesPerCapacity { +export interface ITopNodesPerCapacity { publicKey: string, alias: string, capacity: number, @@ -270,6 +275,6 @@ export interface TopNodesPerCapacity { } export interface INodesRanking { - topByCapacity: TopNodesPerCapacity[]; - topByChannels: TopNodesPerChannels[]; + topByCapacity: ITopNodesPerCapacity[]; + topByChannels: ITopNodesPerChannels[]; } \ 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 4238be7bb..2d1ff8f43 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -153,9 +153,14 @@ export interface RewardStats { } export interface ITopNodesPerChannels { - public_key: string, + publicKey: string, alias: string, - channels: number, + channels?: number, + capacity: number, + firstSeen?: number, + updatedAt?: number, + city?: any, + country?: any, } export interface ITopNodesPerCapacity { diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 49cc7b61e..1aacf7b89 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 } from '../interfaces/node-api.interface'; +import { INodesRanking, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @Injectable({ providedIn: 'root' @@ -69,4 +69,10 @@ export class LightningApiService { this.apiBasePath + '/api/v1/lightning/nodes/rankings/capacity' ); } + + getTopNodesByChannels$(): Observable { + return this.httpClient.get( + this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels' + ); + } } diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index a3885ae7c..9e9405234 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -40,6 +40,13 @@ const routes: Routes = [ type: 'capacity' }, }, + { + path: 'nodes/top-channels', + component: NodesRanking, + data: { + type: 'channels' + }, + }, { path: '**', redirectTo: '' diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html index 9ca4ed6dc..1720e9be6 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html @@ -1,2 +1,5 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html index 4f84b8134..a9043f9d1 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -51,6 +51,18 @@ + + + + + + + + + + + + diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index 1f72b6974..2d90817f9 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -23,8 +23,6 @@ export class TopNodesPerCapacity implements OnInit { this.skeletonRows.push(i); } - console.log(this.widget); - if (this.widget === false) { this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$(); } else { diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html index f581dacff..72c2aa5b1 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -1,37 +1,71 @@ -
- - - - - - - - - - - - - - - - +
+

+ Top 100 nodes by channel count +

+ +
+
AliasChannels
- {{ i + 1 }} - - {{ node.alias }} - - {{ node.channels | number }} -
+ + + + + + + + + + + + + + + - -
AliasChannelsCapacityFirst seenLast updateLocation
- + {{ i + 1 }} - + {{ node.alias }} - + {{ node.channels | number }} + + + + + + + + {{ node?.city?.en ?? '-' }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss index 5335991e3..5af0e8339 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss @@ -1,8 +1,20 @@ +.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; } -.rank { +.full .rank { + width: 5%; +} +.widget .rank { @media (min-width: 767.98px) { width: 13%; } @@ -12,8 +24,8 @@ } } -.alias { - width: 60%; +.full .alias { + width: 10%; overflow: hidden; text-overflow: ellipsis; max-width: 350px; @@ -21,10 +33,52 @@ max-width: 175px; } } -.channels { - width: 27%; +.widget .alias { + width: 55%; + overflow: hidden; + text-overflow: ellipsis; + max-width: 350px; + @media (max-width: 767.98px) { + max-width: 175px; + } +} + +.full .channels { + width: 10%; +} +.widget .channels { + width: 32%; @media (max-width: 767.98px) { padding-left: 0px; padding-right: 0px; } } + +.full .capacity { + width: 15%; + padding-right: 50px; + @media (max-width: 767.98px) { + display: none; + } +} + +.full .timestamp-first { + width: 15%; + @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/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index 46cf21eaa..c2821c596 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface'; +import { LightningApiService } from '../../lightning-api.service'; @Component({ selector: 'app-top-nodes-per-channels', @@ -15,20 +16,22 @@ export class TopNodesPerChannels implements OnInit { topNodesPerChannels$: Observable; skeletonRows: number[] = []; + constructor(private apiService: LightningApiService) {} + ngOnInit(): void { for (let i = 1; i <= (this.widget ? 10 : 100); ++i) { this.skeletonRows.push(i); } - this.topNodesPerChannels$ = this.nodes$.pipe( - map((ranking) => { - if (this.widget === true) { + if (this.widget === false) { + this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$(); + } else { + this.topNodesPerChannels$ = this.nodes$.pipe( + map((ranking) => { return ranking.topByChannels.slice(0, 10); - } else { - return ranking.topByChannels; - } - }) - ) + }) + ); + } } }