Fix node page and display real time data

This commit is contained in:
nymkappa 2022-08-04 11:30:32 +02:00
parent faa59f59bd
commit 3c2e27f778
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
9 changed files with 218 additions and 77 deletions

View file

@ -1,5 +1,6 @@
import logger from '../../logger';
import DB from '../../database';
import nodesApi from './nodes.api';
class ChannelsApi {
public async $getAllChannels(): Promise<any[]> {
@ -181,15 +182,57 @@ class ChannelsApi {
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
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) {

View file

@ -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) {

View file

@ -4,21 +4,13 @@ import DB from '../../database';
class NodesApi {
public async $getNode(public_key: string): Promise<any> {
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<unknown> {
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<any> {
try {
const query = `SELECT * FROM nodes`;
@ -55,7 +96,12 @@ class NodesApi {
public async $getNodeStats(public_key: string): Promise<any> {
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) {

View file

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

View file

@ -2,24 +2,24 @@
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'open'" fragment="open"> Open
<input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'closed'" fragment="closed"> Closed
<input ngbButton type="radio" [value]="'closed'" fragment="closed" i18n="closed">Closed
</label>
</div>
</form>
<table class="table table-borderless" *ngIf="response.channels.length > 1">
<table class="table table-borderless" *ngIf="response.channels.length > 0">
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
<tbody>
<tr *ngFor="let channel of response.channels; let i = index;">
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node_left.public_key === publicKey ? channel.node_right : channel.node_left }"></ng-container>
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node }"></ng-container>
</tr>
</tbody>
</table>
<ngb-pagination *ngIf="response.channels.length > 1" class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<ngb-pagination *ngIf="response.channels.length > 0" class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<table class="table table-borderless" *ngIf="response.channels.length === 0">
<div class="d-flex justify-content-center" i18n="lightning.empty-channels-list">No channels to display</div>
@ -30,7 +30,7 @@
<thead>
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction">&nbsp;</th>
<th class="alias text-left d-none d-md-table-cell" i18n="nodes.alias">Status</th>
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
@ -42,31 +42,41 @@
<div>{{ node.alias || '?' }}</div>
<div class="second-line">
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
<span>{{ node.public_key | shortenString : 10 }}</span>
<span>{{ node.public_key | shortenString : publicKeySize }}</span>
</a>
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
</div>
</td>
<td class="alias text-left d-none d-md-table-cell">
<div class="second-line">{{ node.channels }} channels</div>
<div class="second-line"><app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount></div>
<div class="second-line">
<app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallnode>
{{ node.capacity | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
</div>
</td>
<td class="d-none d-md-table-cell">
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="lightning.inactive">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="lightning.active">Active</span>
<ng-template [ngIf]="channel.status === 2">
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason">Closed</span>
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="lightning.closed">Closed</span>
<ng-template #closingReason>
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
</ng-template>
</ng-template>
</td>
<td class="capacity text-left d-none d-md-table-cell">
{{ node.fee_rate }} <span class="symbol">ppm ({{ node.fee_rate / 10000 | number }}%)</span>
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
</td>
<td class="capacity text-right d-none d-md-table-cell">
<app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount>
</td>
<app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallchannel>
{{ channel.capacity | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
</td>
<td class="capacity text-right">
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
</td>

View file

@ -1,3 +1,9 @@
.second-line {
font-size: 12px;
}
}
.sats {
color: #ffffff66;
font-size: 12px;
top: 0px;
}

View file

@ -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<number>(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);
}

View file

@ -2,8 +2,9 @@
<div class="title-container mb-2" *ngIf="!error">
<h1 class="mb-0">{{ node.alias }}</h1>
<span class="tx-link">
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.public_key | shortenString : 12
}}</a>
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
{{ node.public_key | shortenString : publicKeySize }}
</a>
<app-clipboard [text]="node.public_key"></app-clipboard>
</span>
</div>
@ -22,23 +23,23 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-received">Total capacity</td>
<td i18n="lightning.active-capacity">Active capacity</td>
<td>
<app-sats [satoshis]="node.capacity"></app-sats>
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Total channels</td>
<td i18n="lightning.active-channels">Active channels</td>
<td>
{{ node.channel_active_count }}
{{ node.active_channel_count }}
</td>
</tr>
<tr>
<td i18n="address.total-received">Average channel size</td>
<td i18n="lightning.active-channels-avg">Average channel size</td>
<td>
<app-sats [satoshis]="node.channels_capacity_avg"></app-sats>
<app-fiat [value]="node.channels_capacity_avg" digitsInfo="1.0-0"></app-fiat>
<app-sats [satoshis]="node.avgCapacity"></app-sats>
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr *ngIf="node.country && node.city && node.subdivision">
@ -71,13 +72,13 @@
<tr>
<td i18n="address.total-received">First seen</td>
<td>
<app-timestamp [dateString]="node.first_seen"></app-timestamp>
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Last update</td>
<td>
<app-timestamp [dateString]="node.updated_at"></app-timestamp>
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
</td>
</tr>
<tr>
@ -139,7 +140,7 @@
<br>
<div class="d-flex justify-content-between" *ngIf="!error">
<h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
<h2><span i18n="lightning.all-channels">All channels</span> ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
<div class="d-flex justify-content-end">
<app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
</div>

View file

@ -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 => {