Index daily channel stats and show in dashboard widget

This commit is contained in:
nymkappa 2022-07-06 14:56:10 +02:00
parent 4009a066e0
commit 9000b6b18e
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
13 changed files with 386 additions and 26 deletions

View File

@ -4,7 +4,7 @@ import logger from '../logger';
import { Common } from './common'; import { Common } from './common';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 26; private static currentVersion = 27;
private queryTimeout = 120000; private queryTimeout = 120000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -174,7 +174,7 @@ class DatabaseMigration {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage); this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery(`ALTER TABLE blocks await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL, ADD med_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL ADD avg_fee_rate INT UNSIGNED NULL
`); `);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
@ -265,6 +265,15 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
} }
if (databaseSchemaVersion < 27 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
}
} catch (e) { } catch (e) {
throw e; throw e;
} }

View File

@ -71,6 +71,69 @@ class ChannelsApi {
} }
} }
public async $getChannelsStats(): Promise<any> {
try {
// Feedback from zerofeerouting:
// "I would argue > 5000ppm can be ignored. Channels charging more than .5% fee are ignored by CLN for example."
const ignoredFeeRateThreshold = 5000;
const ignoredBaseFeeThreshold = 5000;
// Capacity
let query = `SELECT AVG(capacity) AS avgCapacity FROM channels WHERE status = 1 ORDER BY capacity`;
const [avgCapacity]: any = await DB.query(query);
query = `SELECT capacity FROM channels WHERE status = 1 ORDER BY capacity`;
let [capacity]: any = await DB.query(query);
capacity = capacity.map(capacity => capacity.capacity);
const medianCapacity = capacity[Math.floor(capacity.length / 2)];
// Fee rates
query = `SELECT node1_fee_rate FROM channels WHERE node1_fee_rate < ${ignoredFeeRateThreshold} AND status = 1`;
let [feeRates1]: any = await DB.query(query);
feeRates1 = feeRates1.map(rate => rate.node1_fee_rate);
query = `SELECT node2_fee_rate FROM channels WHERE node2_fee_rate < ${ignoredFeeRateThreshold} AND status = 1`;
let [feeRates2]: any = await DB.query(query);
feeRates2 = feeRates2.map(rate => rate.node2_fee_rate);
let feeRates = (feeRates1.concat(feeRates2)).sort((a, b) => a - b);
let avgFeeRate = 0;
for (const rate of feeRates) {
avgFeeRate += rate;
}
avgFeeRate /= feeRates.length;
const medianFeeRate = feeRates[Math.floor(feeRates.length / 2)];
// Base fees
query = `SELECT node1_base_fee_mtokens FROM channels WHERE node1_base_fee_mtokens < ${ignoredBaseFeeThreshold} AND status = 1`;
let [baseFees1]: any = await DB.query(query);
baseFees1 = baseFees1.map(rate => rate.node1_base_fee_mtokens);
query = `SELECT node2_base_fee_mtokens FROM channels WHERE node2_base_fee_mtokens < ${ignoredBaseFeeThreshold} AND status = 1`;
let [baseFees2]: any = await DB.query(query);
baseFees2 = baseFees2.map(rate => rate.node2_base_fee_mtokens);
let baseFees = (baseFees1.concat(baseFees2)).sort((a, b) => a - b);
let avgBaseFee = 0;
for (const fee of baseFees) {
avgBaseFee += fee;
}
avgBaseFee /= baseFees.length;
const medianBaseFee = feeRates[Math.floor(baseFees.length / 2)];
return {
avgCapacity: parseInt(avgCapacity[0].avgCapacity, 10),
avgFeeRate: avgFeeRate,
avgBaseFee: avgBaseFee,
medianCapacity: medianCapacity,
medianFeeRate: medianFeeRate,
medianBaseFee: medianBaseFee,
}
} catch (e) {
logger.err(`Cannot calculate channels statistics. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $getChannelsByTransactionId(transactionIds: string[]): Promise<any[]> { public async $getChannelsByTransactionId(transactionIds: string[]): Promise<any[]> {
try { try {
transactionIds = transactionIds.map((id) => '\'' + id + '\''); transactionIds = transactionIds.map((id) => '\'' + id + '\'');

View File

@ -16,7 +16,7 @@ class StatisticsApi {
public async $getLatestStatistics(): Promise<any> { public async $getLatestStatistics(): Promise<any> {
try { try {
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`); const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`);
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 72`); const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 7`);
return { return {
latest: rows[0], latest: rows[0],
previous: rows2[0], previous: rows2[0],

View File

@ -2,6 +2,7 @@
import DB from '../../database'; import DB from '../../database';
import logger from '../../logger'; import logger from '../../logger';
import lightningApi from '../../api/lightning/lightning-api-factory'; import lightningApi from '../../api/lightning/lightning-api-factory';
import channelsApi from '../../api/explorer/channels.api';
import * as net from 'net'; import * as net from 'net';
class LightningStatsUpdater { class LightningStatsUpdater {
@ -124,15 +125,15 @@ class LightningStatsUpdater {
) )
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`; VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`;
await DB.query(query, [ await DB.query(query, [
date.getTime() / 1000, date.getTime() / 1000,
channelsCount, channelsCount,
0, 0,
totalCapacity, totalCapacity,
0, 0,
0, 0,
0 0
]); ]);
// Add one day and continue // Add one day and continue
date.setDate(date.getDate() + 1); date.setDate(date.getDate() + 1);
@ -232,6 +233,8 @@ class LightningStatsUpdater {
} }
} }
const channelStats = await channelsApi.$getChannelsStats();
const query = `INSERT INTO lightning_stats( const query = `INSERT INTO lightning_stats(
added, added,
channel_count, channel_count,
@ -239,7 +242,13 @@ class LightningStatsUpdater {
total_capacity, total_capacity,
tor_nodes, tor_nodes,
clearnet_nodes, clearnet_nodes,
unannounced_nodes unannounced_nodes,
avg_capacity,
avg_fee_rate,
avg_base_fee_mtokens,
med_capacity,
med_fee_rate,
med_base_fee_mtokens
) )
VALUES (NOW(), ?, ?, ?, ?, ?, ?)`; VALUES (NOW(), ?, ?, ?, ?, ?, ?)`;
@ -249,7 +258,13 @@ class LightningStatsUpdater {
total_capacity, total_capacity,
torNodes, torNodes,
clearnetNodes, clearnetNodes,
unannouncedNodes unannouncedNodes,
channelStats.avgCapacity,
channelStats.avgFeeRate,
channelStats.avgBaseFee,
channelStats.medianCapacity,
channelStats.medianFeeRate,
channelStats.medianBaseFee,
]); ]);
logger.info(`Lightning daily stats done.`); logger.info(`Lightning daily stats done.`);
} catch (e) { } catch (e) {

View File

@ -15,7 +15,11 @@ export class ChangeComponent implements OnChanges {
constructor() { } constructor() { }
ngOnChanges(): void { ngOnChanges(): void {
this.change = (this.current - this.previous) / this.previous * 100; if (!this.previous) {
this.change = 0;
} else {
this.change = (this.current - this.previous) / this.previous * 100;
}
} }
} }

View File

@ -0,0 +1,126 @@
<div class="widget-toggler">
<a href="javascript:;" (click)="switchMode('avg')" class="toggler-option"
[ngClass]="{'inactive': mode !== 'avg'}"><small>avg</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="javascript:;" (click)="switchMode('med')" class="toggler-option"
[ngClass]="{'inactive': mode !== 'med'}"><small>med</small></a>
</div>
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
<div class="fee-estimation-container" *ngIf="mode === 'avg'">
<div class="item">
<h5 class="card-title" i18n="ln.average-capacity">Avg Capacity</h5>
<div class="card-text">
<div class="fee-text">
{{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="ln.average-feerate">Avg Fee Rate</h5>
<div class="card-text" i18n-ngbTooltip="ln.average-feerate-desc"
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
placement="bottom">
<div class="fee-text">
{{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="ln.average-basefee">Avg Base Fee</h5>
<div class="card-text" i18n-ngbTooltip="ln.average-basefee-desc"
ngbTooltip="The average base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
<div class="card-text">
<div class="fee-text">
{{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
</span>
</div>
</div>
</div>
</div>
<div class="fee-estimation-container" *ngIf="mode === 'med'">
<div class="item">
<h5 class="card-title" i18n="ln.median-capacity">Med Capacity</h5>
<div class="card-text">
<div class="fee-text">
{{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="ln.average-feerate">Med Fee Rate</h5>
<div class="card-text" i18n-ngbTooltip="ln.median-feerate-desc"
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
placement="bottom">
<div class="fee-text">
{{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="ln.median-basefee">Med Base Fee</h5>
<div class="card-text" i18n-ngbTooltip="ln.median-basefee-desc"
ngbTooltip="The median base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
<div class="card-text">
<div class="fee-text">
{{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
</div>
</div>
<span class="fiat">
<app-change [current]="statistics.latest?.med_base_fee_mtokens" [previous]="statistics.previous?.med_base_fee_mtokens"></app-change>
</span>
</div>
</div>
</div>
</div>
<ng-template #loadingReward>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,101 @@
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
font-size: 1rem;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
display: inline-flex;
}
.green-color {
display: block;
}
}
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.loading-container {
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.widget-toggler {
font-size: 12px;
position: absolute;
top: -20px;
right: 3px;
text-align: right;
}
.toggler-option {
text-decoration: none;
}
.inactive {
color: #ffffff66;
}

View File

@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-channels-statistics',
templateUrl: './channels-statistics.component.html',
styleUrls: ['./channels-statistics.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChannelsStatisticsComponent implements OnInit {
@Input() statistics$: Observable<any>;
mode: string = 'avg';
constructor() { }
ngOnInit(): void {
}
switchMode(mode: 'avg' | 'med') {
this.mode = mode;
}
}

View File

@ -52,6 +52,10 @@ export class LightningApiService {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top'); return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top');
} }
listChannelStats$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/channels/' + publicKey + '/statistics');
}
listStatistics$(): Observable<any> { listStatistics$(): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics'); return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics');
} }

View File

@ -19,6 +19,16 @@
<div class="main-title"> <div class="main-title">
<span i18n="lightning.statistics-title">Channels Statistics</span>&nbsp; <span i18n="lightning.statistics-title">Channels Statistics</span>&nbsp;
</div> </div>
<div class="card-wrapper">
<div class="card" style="height: 123px">
<div class="card-body more-padding">
<app-channels-statistics [statistics$]="statistics$"></app-channels-statistics>
</div>
</div>
</div>
</div>
<div class="col">
<div class="card-wrapper"> <div class="card-wrapper">
</div> </div>

View File

@ -35,7 +35,7 @@ export class LightningDashboardComponent implements OnInit {
map((object) => object.topByChannels), map((object) => object.topByChannels),
); );
this.statistics$ = this.lightningApiService.getLatestStatistics$(); this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
} }
} }

View File

@ -17,6 +17,7 @@ import { LightningStatisticsChartComponent } from './statistics-chart/lightning-
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
import { GraphsModule } from '../graphs/graphs.module'; import { GraphsModule } from '../graphs/graphs.module';
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
LightningDashboardComponent, LightningDashboardComponent,
@ -31,6 +32,7 @@ import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networ
ClosingTypeComponent, ClosingTypeComponent,
LightningStatisticsChartComponent, LightningStatisticsChartComponent,
NodesNetworksChartComponent, NodesNetworksChartComponent,
ChannelsStatisticsComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -2,18 +2,21 @@
<div class="fee-estimation-container"> <div class="fee-estimation-container">
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.average-fee">Capacity</h5> <h5 class="card-title" i18n="mining.average-fee">Capacity</h5>
<div class="card-text" i18n-ngbTooltip="mining.average-fee" <div class="card-text" i18n-ngbTooltip="mining.average-fee" ngbTooltip="Percentage change past week"
ngbTooltip="Percentage change past week" placement="bottom"> placement="bottom">
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount> <div class="fee-text">
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
</div>
<span class="fiat" *ngIf="statistics.previous"> <span class="fiat" *ngIf="statistics.previous">
<app-change [current]="statistics.latest.total_capacity" [previous]="statistics.previous.total_capacity"></app-change> <app-change [current]="statistics.latest.total_capacity" [previous]="statistics.previous.total_capacity">
</app-change>
</span> </span>
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.rewards">Nodes</h5> <h5 class="card-title" i18n="mining.rewards">Nodes</h5>
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc" <div class="card-text" i18n-ngbTooltip="mining.rewards-desc" ngbTooltip="Percentage change past week"
ngbTooltip="Percentage change past week" placement="bottom"> placement="bottom">
<div class="fee-text"> <div class="fee-text">
{{ statistics.latest?.node_count || 0 | number }} {{ statistics.latest?.node_count || 0 | number }}
</div> </div>
@ -24,13 +27,14 @@
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5> <h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" <div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" ngbTooltip="Percentage change past week"
ngbTooltip="Percentage change past week" placement="bottom"> placement="bottom">
<div class="fee-text"> <div class="fee-text">
{{ statistics.latest?.channel_count || 0 | number }} {{ statistics.latest?.channel_count || 0 | number }}
</div> </div>
<span class="fiat" *ngIf="statistics.previous"> <span class="fiat" *ngIf="statistics.previous">
<app-change [current]="statistics.latest.channel_count" [previous]="statistics.previous.channel_count"></app-change> <app-change [current]="statistics.latest.channel_count" [previous]="statistics.previous.channel_count">
</app-change>
</span> </span>
</div> </div>
</div> </div>
@ -73,4 +77,4 @@
</div> </div>
</div> </div>
</div> </div>
</ng-template> </ng-template>