Merge branch 'master' into hunicus/move-on-in-it

This commit is contained in:
hunicus
2024-01-26 08:40:40 -05:00
382 changed files with 130983 additions and 74057 deletions

View File

@@ -37,7 +37,7 @@
<thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="nodedetails text-left">&nbsp;</th>
<th class="status text-left" i18n="status">Status</th>
<th class="status text-left" i18n="transaction.status|Transaction Status">Status</th>
<th class="feerate text-left" *ngIf="status !== 'closed'" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
<th class="feerate text-left" *ngIf="status === 'closed'" i18n="channels.closing_date">Closing date</th>
<th class="liquidity text-right" i18n="lightning.capacity">Capacity</th>

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-channels-statistics',
@@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChannelsStatisticsComponent implements OnInit {
@Input() statistics$: Observable<any>;
@Input() statistics$: Observable<INodesStatistics>;
mode: string = 'avg';
constructor() { }

View File

@@ -57,7 +57,7 @@ export class GroupPreviewComponent implements OnInit {
return of(null);
}
return this.lightningApiService.getNodGroupNodes$(this.groupId);
return this.lightningApiService.getNodeGroup$(this.groupId);
}),
map((nodes) => {
for (const node of nodes) {

View File

@@ -14,7 +14,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Description</td>
<td i18n>Description</td>
<td><div class="description-text">These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us!
</div>
</td>
@@ -70,7 +70,7 @@
<table class="table table-borderless">
<thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="text-left">Connect</th>
<th class="text-left" i18n="lightning.connect-to-node|Connect">Connect</th>
<th class="city text-right d-none d-md-table-cell" i18n="lightning.location">Location</th>
</thead>
<tbody *ngIf="nodes$ | async as response; else skeleton">

View File

@@ -41,7 +41,7 @@ export class GroupComponent implements OnInit {
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
this.nodes$ = this.lightningApiService.getNodeGroup$('mempool.space')
.pipe(
map((nodes) => {
for (const node of nodes) {

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from '../services/state.service';
import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
@@ -9,6 +9,8 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
})
export class LightningApiService {
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
constructor(
private httpClient: HttpClient,
@@ -23,11 +25,51 @@ export class LightningApiService {
});
}
private generateCacheKey(functionName: string, params: any[]): string {
return functionName + JSON.stringify(params);
}
// delete expired cache entries
private cleanExpiredCache(): void {
this.requestCache.forEach((value, key) => {
if (value.expiry < Date.now()) {
this.requestCache.delete(key);
}
});
}
cachedRequest<T, F extends (...args: any[]) => Observable<T>>(
apiFunction: F,
expireAfter: number, // in ms
...params: Parameters<F>
): Observable<T> {
this.cleanExpiredCache();
const cacheKey = this.generateCacheKey(apiFunction.name, params);
if (!this.requestCache.has(cacheKey)) {
const subject = new BehaviorSubject<T | null>(null);
this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter });
apiFunction.bind(this)(...params).pipe(
tap(data => {
subject.next(data as T);
}),
catchError((error) => {
subject.error(error);
return of(null);
}),
shareReplay(1),
).subscribe();
}
return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1));
}
getNode$(publicKey: string): Observable<any> {
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
}
getNodGroupNodes$(name: string): Observable<any[]> {
getNodeGroup$(name: string): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
}

View File

@@ -63,7 +63,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-capacity>
</div>
</div>
</div>
@@ -77,7 +77,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-channels>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { share } from 'rxjs/operators';
import { INodesRanking } from '../../interfaces/node-api.interface';
import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { LightningApiService } from '../lightning-api.service';
@@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightningDashboardComponent implements OnInit, AfterViewInit {
statistics$: Observable<any>;
statistics$: Observable<INodesStatistics>;
nodesRanking$: Observable<INodesRanking>;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
@@ -25,7 +25,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());

View File

@@ -1,5 +1,5 @@
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { switchMap } from 'rxjs/operators';
import { download } from '../../shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';

View File

@@ -1,5 +1,5 @@
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { formatNumber } from '@angular/common';

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-node-statistics',
@@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodeStatisticsComponent implements OnInit {
@Input() statistics$: Observable<any>;
@Input() statistics$: Observable<INodesStatistics>;
constructor() { }

View File

@@ -49,7 +49,7 @@
<tr *ngIf="!node.city && !node.country">
<td i18n="lightning.location">Location</td>
<td>
<span>unknown</span>
<span i18n="unknown">Unknown</span>
</td>
</tr>
</tbody>

View File

@@ -119,7 +119,7 @@
</div>
<ng-template #featurebits let-bits="bits">
<td i18n="lightning.features" class="text-truncate label">Features</td>
<td i18n="transaction.features|Transaction features" class="text-truncate label">Features</td>
<td class="d-flex justify-content-between">
<span class="text-truncate w-90">{{ bits }}</span>
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
@@ -133,11 +133,11 @@
<h5>Raw bits</h5>
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
</div>
<h5>Decoded</h5>
<h5 i18n="lightning.decoded|Decoded">Decoded</h5>
<table class="table table-borderless table-striped table-fixed">
<thead>
<th style="width: 13%">Bit</th>
<th>Name</th>
<th i18n="lightning.as-name">Name</th>
<th style="width: 25%; text-align: right">Required</th>
</thead>
<tbody>

View File

@@ -1,22 +1,28 @@
<div class="map-wrapper" [class]="style">
<div class="map-wrapper" [class]="style" *ngIf="style !== 'graph'">
<ng-container *ngIf="channelsObservable | async">
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div *ngIf="style === 'graph'" class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
</div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div>
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
<div class="spinner-border text-light"></div>
</div>
</ng-container>
</div>
<div class="full-container-graph" *ngIf="style === 'graph'">
<div class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
</div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div *ngIf="channelsObservable | async" class="chart-graph" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div>
</div>

View File

@@ -143,3 +143,55 @@
text-align: center;
margin-top: 100px;
}
.full-container-graph {
display: flex;
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
@media (min-width: 992px) {
height: calc(100vh - 150px);
}
}
.full-container-graph.widget {
min-height: 240px;
height: 240px;
padding: 0px;
}
.full-container-graph.fit-container {
margin: 0;
padding: 0;
height: 100%;
min-height: 100px;
.chart {
padding: 0;
min-height: 100px;
}
}
.chart-graph {
display: flex;
flex: 1;
height: 100%;
padding-top: 30px;
padding-bottom: 20px;
padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
}

View File

@@ -6,8 +6,7 @@ import { AssetsService } from '../../services/assets.service';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { EChartsOption, registerMap } from 'echarts';
import 'echarts-gl';
import { EChartsOption, echarts } from '../../graphs/echarts';
import { isMobile } from '../../shared/common.utils';
@Component({
@@ -66,6 +65,7 @@ export class NodesChannelsMap implements OnInit {
}
if (this.style === 'graph') {
this.center = [0, 5];
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
}
@@ -88,7 +88,7 @@ export class NodesChannelsMap implements OnInit {
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
[params.get('public_key') ?? undefined]
).pipe(tap((data) => {
registerMap('world', data[0]);
echarts.registerMap('world', data[0]);
const channelsLoc = [];
const nodes = [];
@@ -239,7 +239,6 @@ export class NodesChannelsMap implements OnInit {
title: title ?? undefined,
tooltip: {},
geo: {
top: 75,
animation: false,
silent: true,
center: this.center,

View File

@@ -1,7 +1,7 @@
import { formatNumber } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router';
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
import { EChartsOption, TreemapSeriesOption } from '../../graphs/echarts';
import { Observable, share, switchMap, tap } from 'rxjs';
import { lerpColor } from '../../shared/graphs.utils';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
@@ -18,7 +18,7 @@ import { StateService } from '../../services/state.service';
export class NodeChannels implements OnChanges {
@Input() publicKey: string;
chartInstance: ECharts;
chartInstance: any;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
@@ -129,7 +129,7 @@ export class NodeChannels implements OnChanges {
};
}
onChartInit(ec: ECharts): void {
onChartInit(ec: any): void {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {

View File

@@ -3,7 +3,7 @@ import { SeoService } from '../../services/seo.service';
import { ApiService } from '../../services/api.service';
import { Observable, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs';
import { AssetsService } from '../../services/assets.service';
import { EChartsOption, registerMap } from 'echarts';
import { EChartsOption, echarts } from '../../graphs/echarts';
import { lerpColor } from '../../shared/graphs.utils';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -63,7 +63,7 @@ export class NodesMap implements OnInit, OnChanges {
this.assetsService.getWorldMapJson$,
this.nodes$
).pipe(tap((data) => {
registerMap('world', data[0]);
echarts.registerMap('world', data[0]);
let maxLiquidity = data[1].maxLiquidity;
let inputNodes: any[] = data[1].nodes;
@@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges {
node.public_key,
node.alias,
node.capacity,
node.channels,
node.active_channel_count,
node.country,
node.iso_code,
]);
@@ -114,7 +114,7 @@ export class NodesMap implements OnInit, OnChanges {
node[3], // Alias
node[2], // Public key
node[5], // Channels
node[6].en, // Country
node[6]?.en, // Country
node[7], // ISO Code
]);
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic, LineSeriesOption} from 'echarts';
import { echarts, EChartsOption, LineSeriesOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { formatNumber } from '@angular/common';
@@ -82,9 +82,9 @@ export class NodesNetworksChartComponent implements OnInit {
firstRun = false;
this.miningWindowPreference = timespan;
this.isLoading = true;
return this.lightningApiService.listStatistics$(timespan)
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
.pipe(
tap((response) => {
tap((response:any) => {
const data = response.body;
const chartData = {
tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]),
@@ -152,7 +152,7 @@ export class NodesNetworksChartComponent implements OnInit {
opacity: 0.5,
},
stack: 'Total',
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
{ offset: 0, color: '#D81B60' },
{ offset: 1, color: '#D81B60AA' },
]),
@@ -174,7 +174,7 @@ export class NodesNetworksChartComponent implements OnInit {
opacity: 0.5,
},
stack: 'Total',
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
{ offset: 0, color: '#be7d4c' },
{ offset: 1, color: '#be7d4cAA' },
]),
@@ -195,7 +195,7 @@ export class NodesNetworksChartComponent implements OnInit {
opacity: 0.5,
},
stack: 'Total',
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
{ offset: 0, color: '#FFB300' },
{ offset: 1, color: '#FFB300AA' },
]),
@@ -216,7 +216,7 @@ export class NodesNetworksChartComponent implements OnInit {
opacity: 0.5,
},
stack: 'Total',
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
{ offset: 0, color: '#7D4698' },
{ offset: 1, color: '#7D4698AA' },
]),

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
import { map, Observable, share, tap } from 'rxjs';
import { chartColors } from '../../app.constants';
import { ApiService } from '../../services/api.service';

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs';
import { chartColors } from '../../app.constants';
import { ApiService } from '../../services/api.service';

View File

@@ -1,7 +1,7 @@
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
<app-top-nodes-per-capacity [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'capacity'">
</app-top-nodes-per-capacity>
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'">
<app-top-nodes-per-channels [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'channels'">
</app-top-nodes-per-channels>
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>

View File

@@ -1,5 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { LightningApiService } from '../lightning-api.service';
import { share } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-nodes-ranking',
@@ -9,10 +13,15 @@ import { ActivatedRoute } from '@angular/router';
})
export class NodesRanking implements OnInit {
type: string;
statistics$: Observable<INodesStatistics>;
constructor(private route: ActivatedRoute) {}
constructor(
private route: ActivatedRoute,
private lightningApiService: LightningApiService,
) {}
ngOnInit(): void {
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
this.route.data.subscribe(data => {
this.type = data.type;
});

View File

@@ -16,8 +16,8 @@
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th>
</thead>
<tbody *ngIf="topNodesPerCapacity$ | async as nodes">
<tr *ngFor="let node of nodes;">
<tbody *ngIf="topNodesPerCapacity$ | async as data">
<tr *ngFor="let node of data.nodes;">
<td class="pool text-left">
<div class="tooltip-custom d-block w-100">
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
@@ -27,12 +27,14 @@
</td>
<td class="text-right">
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<span class="capacity-ratio">&nbsp;({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
</td>
<td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}">
<app-fiat [value]="node.capacity"></app-fiat>
</td>
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
{{ node.channels | number }}
<span class="capacity-ratio">&nbsp;({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
</td>
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>

View File

@@ -41,6 +41,11 @@ tr, td, th {
}
}
.capacity-ratio {
font-size: 12px;
color: darkgrey;
}
.fiat {
width: 15%;
@media (min-width: 768px) and (max-width: 991px) {

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
import { combineLatest, map, Observable } from 'rxjs';
import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
import { SeoService } from '../../../services/seo.service';
import { StateService } from '../../../services/state.service';
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
@@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service';
})
export class TopNodesPerCapacity implements OnInit {
@Input() nodes$: Observable<INodesRanking>;
@Input() statistics$: Observable<INodesStatistics>;
@Input() widget: boolean = false;
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>;
skeletonRows: number[] = [];
currency$: Observable<string>;
@@ -39,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit {
}
if (this.widget === false) {
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe(
map((ranking) => {
this.topNodesPerCapacity$ = combineLatest([
this.apiService.getTopNodesByCapacity$(),
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
@@ -49,13 +54,28 @@ export class TopNodesPerCapacity implements OnInit {
iso: ranking[i].iso_code,
};
}
return ranking;
return {
nodes: ranking,
statistics: {
totalCapacity: statistics.latest.total_capacity,
totalChannels: statistics.latest.channel_count,
}
}
})
);
} else {
this.topNodesPerCapacity$ = this.nodes$.pipe(
map((ranking) => {
return ranking.topByCapacity.slice(0, 6);
this.topNodesPerCapacity$ = combineLatest([
this.nodes$,
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
return {
nodes: ranking.topByCapacity.slice(0, 6),
statistics: {
totalCapacity: statistics.latest.total_capacity,
}
}
})
);
}

View File

@@ -16,8 +16,8 @@
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>
</thead>
<tbody *ngIf="topNodesPerChannels$ | async as nodes">
<tr *ngFor="let node of nodes;">
<tbody *ngIf="topNodesPerChannels$ | async as data">
<tr *ngFor="let node of data.nodes;">
<td class="pool text-left">
<div class="tooltip-custom d-block w-100">
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
@@ -27,9 +27,11 @@
</td>
<td class="text-right">
{{ node.channels ? (node.channels | number) : '~' }}
<span class="capacity-ratio">&nbsp;({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
</td>
<td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right">
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<span class="capacity-ratio">&nbsp;({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
</td>
<td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right">
<app-fiat [value]="node.capacity"></app-fiat>

View File

@@ -44,6 +44,11 @@ tr, td, th {
}
}
.capacity-ratio {
font-size: 12px;
color: darkgrey;
}
.geolocation {
@media (min-width: 768px) and (max-width: 991px) {
display: none !important;

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
import { combineLatest, map, Observable } from 'rxjs';
import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
import { SeoService } from '../../../services/seo.service';
import { StateService } from '../../../services/state.service';
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
@@ -14,12 +14,13 @@ import { LightningApiService } from '../../lightning-api.service';
})
export class TopNodesPerChannels implements OnInit {
@Input() nodes$: Observable<INodesRanking>;
@Input() statistics$: Observable<INodesStatistics>;
@Input() widget: boolean = false;
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>;
skeletonRows: number[] = [];
currency$: Observable<string>;
constructor(
private apiService: LightningApiService,
private stateService: StateService,
@@ -37,8 +38,12 @@ export class TopNodesPerChannels implements OnInit {
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`);
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
map((ranking) => {
this.topNodesPerChannels$ = combineLatest([
this.apiService.getTopNodesByChannels$(),
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
@@ -47,12 +52,22 @@ export class TopNodesPerChannels implements OnInit {
iso: ranking[i].iso_code,
};
}
return ranking;
return {
nodes: ranking,
statistics: {
totalChannels: statistics.latest.channel_count,
totalCapacity: statistics.latest.total_capacity,
}
}
})
);
} else {
this.topNodesPerChannels$ = this.nodes$.pipe(
map((ranking) => {
this.topNodesPerChannels$ = combineLatest([
this.nodes$,
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking.topByChannels) {
ranking.topByChannels[i].geolocation = <GeolocationData>{
country: ranking.topByChannels[i].country?.en,
@@ -61,7 +76,12 @@ export class TopNodesPerChannels implements OnInit {
iso: ranking.topByChannels[i].iso_code,
};
}
return ranking.topByChannels.slice(0, 6);
return {
nodes: ranking.topByChannels.slice(0, 6),
statistics: {
totalChannels: statistics.latest.channel_count,
}
}
})
);
}

View File

@@ -20,7 +20,7 @@ export class NodesRankingsDashboard implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`Top lightning nodes`);
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See top the Lightning network nodes ranked by liquidity, connectivity, and age.`);
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See the top Lightning network nodes ranked by liquidity, connectivity, and age.`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
@@ -81,9 +81,9 @@ export class LightningStatisticsChartComponent implements OnInit {
firstRun = false;
this.miningWindowPreference = timespan;
this.isLoading = true;
return this.lightningApiService.listStatistics$(timespan)
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
.pipe(
tap((response) => {
tap((response:any) => {
const data = response.body;
this.prepareChartOptions({
channel_count: data.map(val => [val.added * 1000, val.channel_count]),
@@ -132,7 +132,7 @@ export class LightningStatisticsChartComponent implements OnInit {
animation: false,
color: [
'#FFB300',
new graphic.LinearGradient(0, 0.75, 0, 1, [
new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
{ offset: 0, color: '#D81B60' },
{ offset: 1, color: '#D81B60AA' },
]),