From 3c2e27f778dc0628ab88be1036a6b343a5bcf97a Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 11:30:32 +0200 Subject: [PATCH] Fix node page and display real time data --- backend/src/api/explorer/channels.api.ts | 66 +++++++++++-- backend/src/api/explorer/channels.routes.ts | 6 +- backend/src/api/explorer/nodes.api.ts | 96 ++++++++++++++----- backend/src/api/explorer/nodes.routes.ts | 6 ++ .../channels-list.component.html | 38 +++++--- .../channels-list.component.scss | 8 +- .../channels-list/channels-list.component.ts | 42 +++++--- .../app/lightning/node/node.component.html | 23 ++--- .../src/app/lightning/node/node.component.ts | 10 +- 9 files changed, 218 insertions(+), 77 deletions(-) diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index 79aeebb97..9928cc85b 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -1,5 +1,6 @@ import logger from '../../logger'; import DB from '../../database'; +import nodesApi from './nodes.api'; class ChannelsApi { public async $getAllChannels(): Promise { @@ -181,15 +182,57 @@ class ChannelsApi { public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise { try { - // Default active and inactive channels - let statusQuery = '< 2'; - // Closed channels only - if (status === 'closed') { - statusQuery = '= 2'; + let channelStatusFilter; + if (status === 'open') { + channelStatusFilter = '< 2'; + } else if (status === 'closed') { + channelStatusFilter = '= 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)); + + // Channels originating from node + let query = ` + SELECT node2.alias, node2.public_key, channels.status, channels.node1_fee_rate, + channels.capacity, channels.short_id, channels.id + FROM channels + JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key + WHERE node1_public_key = ? AND channels.status ${channelStatusFilter} + `; + const [channelsFromNode]: any = await DB.query(query, [public_key, index, length]); + + // Channels incoming to node + query = ` + SELECT node1.alias, node1.public_key, channels.status, channels.node2_fee_rate, + channels.capacity, channels.short_id, channels.id + FROM channels + JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key + WHERE node2_public_key = ? AND channels.status ${channelStatusFilter} + `; + const [channelsToNode]: any = await DB.query(query, [public_key, index, length]); + + let allChannels = channelsFromNode.concat(channelsToNode); + allChannels.sort((a, b) => { + return b.capacity - a.capacity; + }); + allChannels = allChannels.slice(index, index + length); + + const channels: any[] = [] + for (const row of allChannels) { + const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key); + channels.push({ + status: row.status, + capacity: row.capacity ?? 0, + short_id: row.short_id, + id: row.id, + fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0, + node: { + alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20), + public_key: row.public_key, + channels: activeChannelsStats.active_channel_count ?? 0, + capacity: activeChannelsStats.capacity ?? 0, + } + }); + } + return channels; } catch (e) { logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e)); @@ -205,7 +248,12 @@ class ChannelsApi { 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 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) { diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 495eec789..bbb075aa6 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -46,9 +46,11 @@ class ChannelsRoutes { } 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 channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status); const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 96da7d1d5..6fba07449 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -4,21 +4,13 @@ import DB from '../../database'; class NodesApi { public async $getNode(public_key: string): Promise { try { - const query = ` - SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, - geo_names_country.names as country, geo_names_subdivision.names as subdivision, - (SELECT Count(*) - FROM channels - WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count, - (SELECT Count(*) - FROM channels - WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count, - (SELECT Sum(capacity) - FROM channels - WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity, - (SELECT Avg(capacity) - FROM channels - WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg + // General info + let query = ` + SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen, + UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets, + as_number, city_id, country_id, subdivision_id, longitude, latitude, + geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, + geo_names_country.names as country, geo_names_subdivision.names as subdivision FROM nodes LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id @@ -27,21 +19,70 @@ class NodesApi { LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' WHERE public_key = ? `; - const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]); - if (rows.length > 0) { - rows[0].as_organization = JSON.parse(rows[0].as_organization); - rows[0].subdivision = JSON.parse(rows[0].subdivision); - rows[0].city = JSON.parse(rows[0].city); - rows[0].country = JSON.parse(rows[0].country); - return rows[0]; + let [rows]: any[] = await DB.query(query, [public_key]); + if (rows.length === 0) { + throw new Error(`This node does not exist, or our node is not seeing it yet`); } - return null; + + const node = rows[0]; + node.as_organization = JSON.parse(node.as_organization); + node.subdivision = JSON.parse(node.subdivision); + node.city = JSON.parse(node.city); + node.country = JSON.parse(node.country); + + // Active channels and capacity + const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key); + node.active_channel_count = activeChannelsStats.active_channel_count ?? 0; + node.capacity = activeChannelsStats.capacity ?? 0; + + // Opened channels count + query = ` + SELECT count(short_id) as opened_channel_count + FROM channels + WHERE status != 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + [rows] = await DB.query(query, [public_key, public_key]); + node.opened_channel_count = 0; + if (rows.length > 0) { + node.opened_channel_count = rows[0].opened_channel_count; + } + + // Closed channels count + query = ` + SELECT count(short_id) as closed_channel_count + FROM channels + WHERE status = 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + [rows] = await DB.query(query, [public_key, public_key]); + node.closed_channel_count = 0; + if (rows.length > 0) { + node.closed_channel_count = rows[0].closed_channel_count; + } + + return node; } catch (e) { - logger.err('$getNode error: ' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`); throw e; } } + public async $getActiveChannelsStats(node_public_key: string): Promise { + const query = ` + SELECT count(short_id) as active_channel_count, sum(capacity) as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + const [rows]: any[] = await DB.query(query, [node_public_key, node_public_key]); + if (rows.length > 0) { + return { + active_channel_count: rows[0].active_channel_count, + capacity: rows[0].capacity + }; + } else { + return null; + } + } + public async $getAllNodes(): Promise { try { const query = `SELECT * FROM nodes`; @@ -55,7 +96,12 @@ class NodesApi { public async $getNodeStats(public_key: string): Promise { try { - const query = `SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels 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) { diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 83e3c393e..a850b6a09 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -35,6 +35,9 @@ class NodesRoutes { res.status(404).send('Node not found'); return; } + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); @@ -44,6 +47,9 @@ class NodesRoutes { private async $getHistoricalNodeStats(req: Request, res: Response) { try { const statistics = await nodesApi.$getNodeStats(req.params.public_key); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); 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 82283f689..b95cddf8d 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -2,24 +2,24 @@
- +
- +
- +
No channels to display
@@ -30,7 +30,7 @@ - + @@ -42,31 +42,41 @@
{{ node.alias || '?' }}
+ + + {{ channel.capacity | amountShortener: 1 }} + sats + + 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 35a6ce0bc..ba7b0a3b5 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.scss +++ b/frontend/src/app/lightning/channels-list/channels-list.component.scss @@ -1,3 +1,9 @@ .second-line { font-size: 12px; -} \ No newline at end of file +} + +.sats { + color: #ffffff66; + font-size: 12px; + top: 0px; +} 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 4060d36da..6172a4a99 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs'; -import { map, startWith, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, merge, Observable } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { isMobile } from 'src/app/shared/common.utils'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -18,11 +19,13 @@ export class ChannelsListComponent implements OnInit, OnChanges { // @ts-ignore paginationSize: 'sm' | 'lg' = 'md'; paginationMaxSize = 10; - itemsPerPage = 25; + itemsPerPage = 10; page = 1; channelsPage$ = new BehaviorSubject(1); channelStatusForm: FormGroup; defaultStatus = 'open'; + status = 'open'; + publicKeySize = 25; constructor( private lightningApiService: LightningApiService, @@ -31,9 +34,12 @@ export class ChannelsListComponent implements OnInit, OnChanges { this.channelStatusForm = this.formBuilder.group({ status: [this.defaultStatus], }); + if (isMobile()) { + this.publicKeySize = 12; + } } - ngOnInit() { + ngOnInit(): void { if (document.body.clientWidth < 670) { this.paginationSize = 'sm'; this.paginationMaxSize = 3; @@ -41,28 +47,36 @@ export class ChannelsListComponent implements OnInit, OnChanges { } ngOnChanges(): void { - this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false }) - this.channelsStatusChangedEvent.emit(this.defaultStatus); + this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false }); + this.channelsPage$.next(1); - this.channels$ = combineLatest([ + this.channels$ = merge( this.channelsPage$, - this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus)) - ]) + this.channelStatusForm.get('status').valueChanges, + ) .pipe( - switchMap(([page, status]) => { - this.channelsStatusChangedEvent.emit(status); - return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status); + tap((val) => { + if (typeof val === 'string') { + this.status = val; + this.page = 1; + } else if (typeof val === 'number') { + this.page = val; + } + }), + switchMap(() => { + this.channelsStatusChangedEvent.emit(this.status); + return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status); }), map((response) => { return { channels: response.body, - totalItems: parseInt(response.headers.get('x-total-count'), 10) + totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1 }; }), ); } - pageChange(page: number) { + pageChange(page: number): void { this.channelsPage$.next(page); } diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index cb0e5ed43..ac50ed51b 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -2,8 +2,9 @@ @@ -22,23 +23,23 @@
Node Alias  StatusStatus Fee Rate Capacity Channel ID
{{ node.channels }} channels
-
+
+ + + {{ node.capacity | amountShortener: 1 }} + sats + +
- Inactive - Active + Inactive + Active - Closed + Closed - {{ node.fee_rate }} ppm ({{ node.fee_rate / 10000 | number }}%) + {{ channel.fee_rate }} ppm ({{ channel.fee_rate / 10000 | number }}%) - - {{ channel.short_id }}
- + - + - + @@ -71,13 +72,13 @@ @@ -139,7 +140,7 @@
-

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

+

All channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index a8d487938..6f9358090 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -5,6 +5,7 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; import { getFlagEmoji } from 'src/app/shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-node', @@ -23,11 +24,17 @@ export class NodeComponent implements OnInit { error: Error; publicKey: string; + publicKeySize = 99; + constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, private seoService: SeoService, - ) { } + ) { + if (isMobile()) { + this.publicKeySize = 12; + } + } ngOnInit(): void { this.node$ = this.activatedRoute.paramMap @@ -59,6 +66,7 @@ export class NodeComponent implements OnInit { }); } node.socketsObject = socketsObject; + node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count); return node; }), catchError(err => {
Total capacityActive capacity
Total channelsActive channels - {{ node.channel_active_count }} + {{ node.active_channel_count }}
Average channel sizeAverage channel size - - + +
First seen - +
Last update - +