Merge branch 'master' into fix/difficulty-api

This commit is contained in:
wiz
2022-08-24 20:01:52 +09:00
committed by GitHub
63 changed files with 1125 additions and 472 deletions

View File

@@ -41,7 +41,9 @@
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.LIGHTNING">
<a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon></a>
<a class="nav-link" [routerLink]="['/lightning' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" i18n-title="master-page.lightning" title="Lightning Explorer"></fa-icon>
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
</a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>

View File

@@ -182,3 +182,14 @@ nav {
width: 140px;
height: 35px;
}
.beta {
display: inline;
position: absolute;
margin: 24px 0px 0px -15px;
font-size: 8px;
@media (max-width: 767.98px) {
margin: 33px 0px 0px -19px;
font-size: 7px;
}
}

View File

@@ -20,7 +20,7 @@
<div class="col">
<table class="table table-borderless smaller-text table-sm table-tx-vin">
<tbody>
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length>12)?tx.vin.slice(0, 10): tx.vin.slice(0, 12)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
@@ -146,9 +146,9 @@
</td>
</tr>
</ng-template>
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
<tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
</td>
</tr>
</tbody>
@@ -158,7 +158,7 @@
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-sm table-tx-vout">
<tbody>
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > 12) ? tx.vout.slice(0, 10) : tx.vout.slice(0, 12)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
<tr [ngClass]="{
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
@@ -257,9 +257,9 @@
</td>
</tr>
</ng-template>
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
<tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
</td>
</tr>
</tbody>

View File

@@ -26,6 +26,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() paginated = false;
@Input() outputIndex: number;
@Input() address: string = '';
@Input() rowLimit = 12;
@Input() channels: { inputs: any[], outputs: any[] };
@Output() loadMore = new EventEmitter();
@@ -36,7 +38,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
showDetails$ = new BehaviorSubject<boolean>(false);
outspends: Outspend[][] = [];
assetsMinimal: any;
channels: { inputs: any[], outputs: any[] };
constructor(
public stateService: StateService,
@@ -127,7 +128,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
});
const txIds = this.transactions.map((tx) => tx.txid);
this.refreshOutspends$.next(txIds);
this.refreshChannels$.next(txIds);
if (!this.channels) {
this.refreshChannels$.next(txIds);
}
}
onScroll() {

View File

@@ -188,4 +188,4 @@ export interface IOldestNodes {
updatedAt?: number,
city?: any,
country?: any,
}
}

View File

@@ -16,3 +16,9 @@
color: #ffffff66;
font-size: 12px;
}
@media (max-width: 768px) {
.box {
margin-bottom: 20px;
}
}

View File

@@ -1,4 +1,5 @@
<div class="container-xl" *ngIf="(channel$ | async) as channel">
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.channel">Lightning channel</h5>
<div class="title-container">
<h1 class="mb-0">{{ channel.short_id }}</h1>
<span class="tx-link">
@@ -14,7 +15,7 @@
<div class="clearfix"></div>
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
<div class="box">
@@ -30,32 +31,6 @@
<td i18n="address.total-sent">Last update</td>
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
</tr>
<tr>
<td i18n="address.total-sent">Opening transaction</td>
<td>
<a [routerLink]="['/tx' | relativeUrl, channel.transaction_id + ':' + channel.transaction_vout]" >
<span>{{ channel.transaction_id | shortenString : 10 }}</span>
</a>
<app-clipboard [text]="channel.transaction_id"></app-clipboard>
</td>
</tr>
<ng-template [ngIf]="channel.closing_transaction_id">
<tr *ngIf="channel.closing_transaction_id">
<td i18n="address.total-sent">Closing transaction</td>
<td>
<a [routerLink]="['/tx' | relativeUrl, channel.closing_transaction_id]" >
<span>{{ channel.closing_transaction_id | shortenString : 10 }}</span>
</a>
<app-clipboard [text]="channel.closing_transaction_id"></app-clipboard>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Closing type</td>
<td>
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
@@ -82,8 +57,23 @@
</div>
<div class="col">
<app-channel-box [channel]="channel.node_right"></app-channel-box>
</div>
</div>
</div>
<br>
<ng-container *ngIf="transactions$ | async as transactions">
<ng-template [ngIf]="transactions[0]">
<h3>Opening transaction</h3>
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
</ng-template>
<ng-template [ngIf]="transactions[1]">
<div class="closing-header">
<h3 style="margin: 0;">Closing transaction</h3>&nbsp;&nbsp;<app-closing-type [type]="channel.closing_reason"></app-closing-type>
</div>
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
</ng-template>
</ng-container>
</div>

View File

@@ -39,3 +39,16 @@ app-fiat {
margin-left: 10px;
}
}
.closing-header {
display: flex;
flex-direction: row;
margin-bottom: 1rem;
align-items: center;
}
@media (max-width: 768px) {
h3 {
font-size: 1.4rem;
}
}

View File

@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { forkJoin, Observable, of, share, zip } from 'rxjs';
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { SeoService } from 'src/app/services/seo.service';
import { LightningApiService } from '../lightning-api.service';
@@ -13,13 +15,15 @@ import { LightningApiService } from '../lightning-api.service';
})
export class ChannelComponent implements OnInit {
channel$: Observable<any>;
channelGeo$: Observable<number[]>;
transactions$: Observable<any>;
error: any = null;
channelGeo: number[] = [];
constructor(
private lightningApiService: LightningApiService,
private activatedRoute: ActivatedRoute,
private seoService: SeoService,
private electrsApiService: ElectrsApiService,
) { }
ngOnInit(): void {
@@ -30,28 +34,41 @@ export class ChannelComponent implements OnInit {
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe(
tap((data) => {
if (!data.node_left.longitude || !data.node_left.latitude ||
!data.node_right.longitude || !data.node_right.latitude) {
this.channelGeo = [];
} else {
this.channelGeo = [
data.node_left.public_key,
data.node_left.alias,
data.node_left.longitude, data.node_left.latitude,
data.node_right.public_key,
data.node_right.alias,
data.node_right.longitude, data.node_right.latitude,
];
}
}),
catchError((err) => {
this.error = err;
return of(null);
})
);
})
}),
shareReplay(),
);
this.channelGeo$ = this.channel$.pipe(
map((data) => {
if (!data.node_left.longitude || !data.node_left.latitude ||
!data.node_right.longitude || !data.node_right.latitude) {
return [];
} else {
return [
data.node_left.public_key,
data.node_left.alias,
data.node_left.longitude, data.node_left.latitude,
data.node_right.public_key,
data.node_right.alias,
data.node_right.longitude, data.node_right.latitude,
];
}
}),
);
this.transactions$ = this.channel$.pipe(
switchMap((data) => {
return zip([
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
]);
}),
);
}
}

View File

@@ -19,7 +19,11 @@
</tbody>
</table>
<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>
<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>
@@ -83,7 +87,7 @@
</ng-template>
<ng-template #skeleton>
<h2 class="float-left">Channels</h2>
<h2 class="float-left" i18n="lightning.channels">Channels</h2>
<table class="table table-borderless">
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>

View File

@@ -47,7 +47,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
}
ngOnChanges(): void {
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false });
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: true });
this.channelsPage$.next(1);
this.channels$ = merge(
@@ -70,7 +70,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
map((response) => {
return {
channels: response.body,
totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1
totalItems: parseInt(response.headers.get('x-total-count'), 10)
};
}),
);

View File

@@ -32,7 +32,7 @@ export class LightningApiService {
}
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
let params = new HttpParams()
const params = new HttpParams()
.set('public_key', publicKey)
.set('index', index)
.set('status', status)
@@ -66,13 +66,13 @@ export class LightningApiService {
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
return this.httpClient.get<ITopNodesPerCapacity[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/capacity'
this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
);
}
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
return this.httpClient.get<ITopNodesPerChannels[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels'
this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
);
}

View File

@@ -37,7 +37,7 @@
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<app-nodes-per-isp-chart [widget]="true"></app-nodes-per-isp-chart>
<div class="mt-2"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
<div style="margin-top: 5px"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@@ -49,17 +49,17 @@
<h5 class="card-title mt-3" i18n="lightning.network-history">Lightning network history</h5>
<app-lightning-statistics-chart [widget]=true></app-lightning-statistics-chart>
<app-nodes-networks-chart [widget]=true></app-nodes-networks-chart>
<div class="mt-1"><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
<div><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<!-- Top nodes per capacity -->
<div class="col">
<div class="card">
<div class="card" style="height: 409px">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
<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>
@@ -70,10 +70,10 @@
<!-- Top nodes per channels -->
<div class="col">
<div class="card">
<div class="card" style="height: 409px">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
<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>

View File

@@ -29,6 +29,7 @@ import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-ch
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
@NgModule({
declarations: [
@@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
TopNodesPerCapacity,
OldestNodes,
NodesRankingsDashboard,
NodeChannels,
],
imports: [
CommonModule,
@@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
TopNodesPerCapacity,
OldestNodes,
NodesRankingsDashboard,
NodeChannels,
],
providers: [
LightningApiService,

View File

@@ -39,14 +39,14 @@ const routes: Routes = [
component: NodesRankingsDashboard,
},
{
path: 'nodes/top-capacity',
path: 'nodes/rankings/liquidity',
component: NodesRanking,
data: {
type: 'capacity'
},
},
{
path: 'nodes/top-channels',
path: 'nodes/rankings/connectivity',
component: NodesRanking,
data: {
type: 'channels'

View File

@@ -1,129 +1,5 @@
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
padding: 0px 15px;
width: 100%;
/* min-height: 500px; */
height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
};
margin-top: 25px;
margin-bottom: 25px;
min-height: 100%;
}
/*
.chart {
width: 100%;
height: 100%;
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;
}
}
*/
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@@ -25,7 +25,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
export class NodeStatisticsChartComponent implements OnInit {
@Input() publicKey: string;
@Input() right: number | string = 65;
@Input() left: number | string = 55;
@Input() left: number | string = 45;
@Input() widget = false;
miningWindowPreference: string;
@@ -96,7 +96,7 @@ export class NodeStatisticsChartComponent implements OnInit {
],
grid: {
top: 30,
bottom: 70,
bottom: 20,
right: this.right,
left: this.left,
},

View File

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { OpenGraphService } from 'src/app/services/opengraph.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
import { LightningApiService } from '../lightning-api.service';
import { isMobile } from '../../shared/common.utils';

View File

@@ -1,4 +1,5 @@
<div class="container-xl" *ngIf="(node$ | async) as node">
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
<div class="title-container mb-2" *ngIf="!error">
<h1 class="mb-0">{{ node.alias }}</h1>
<span class="tx-link">
@@ -42,24 +43,10 @@
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr *ngIf="node.country && node.city && node.subdivision">
<tr *ngIf="node.geolocation">
<td i18n="location">Location</td>
<td>
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
<br>
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
<span class="link">{{ node.country.en }}</span>
&nbsp;
<span class="flag">{{ node.flag }}</span>
</a>
</td>
</tr>
<tr *ngIf="node.country && !node.city">
<td i18n="location">Location</td>
<td>
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
{{ node.country.en }} {{ node.flag }}
</a>
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
</td>
</tr>
</tbody>
@@ -131,15 +118,26 @@
</button>
</div>
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
<div *ngIf="!error">
<div class="row">
<div class="col-sm">
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
</div>
<div class="col-sm">
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div>
</div>
<div class="d-flex justify-content-between" *ngIf="!error">
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
<div class="d-flex justify-content-between">
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
</div>
<app-channels-list [publicKey]="node.public_key"
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
</div>
<app-channels-list *ngIf="!error" [publicKey]="node.public_key"
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
</div>

View File

@@ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
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';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-node',
@@ -58,7 +58,6 @@ export class NodeComponent implements OnInit {
} else if (socket.indexOf('onion') > -1) {
label = 'Tor';
}
node.flag = getFlagEmoji(node.iso_code);
socketsObject.push({
label: label,
socket: node.public_key + '@' + socket,
@@ -66,6 +65,19 @@ export class NodeComponent implements OnInit {
}
node.socketsObject = socketsObject;
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
if (!node?.country && !node?.city &&
!node?.subdivision && !node?.iso) {
node.geolocation = null;
} else {
node.geolocation = <GeolocationData>{
country: node.country?.en,
city: node.city?.en,
subdivision: node.subdivision?.en,
iso: node.iso_code,
};
}
return node;
}),
catchError(err => {

View File

@@ -1,14 +1,21 @@
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div [style]="style === 'widget' ? 'height: 250px' : ''">
<div *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 *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 class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
(chartFinished)="onChartFinished($event)">
</div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div>
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -10,25 +10,24 @@
.full-container {
padding: 0px 15px;
width: 100%;
min-height: 500px;
min-height: 600px;
height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
}
}
.full-container.nodepage {
min-height: 400px;
margin-top: 25px;
margin-bottom: 25px;
}
.full-container.channelpage {
min-height: 400px;
margin-top: 25px;
margin-bottom: 25px;
min-height: 100%;
}
.full-container.widget {
height: 250px;
min-height: 250px;
}
.full-container.fit-container {
margin: 0;
padding: 0;
@@ -41,25 +40,6 @@
}
}
.widget {
width: 90vw;
margin-left: auto;
margin-right: auto;
height: 250px;
-webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%);
@media (max-width: 767.98px) {
width: 100vw;
}
}
.widget > .chart {
min-height: 250px;
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
@media (max-width: 767.98px) {
padding-bottom: 0px;
}
}
.chart {
min-height: 500px;
width: 100%;
@@ -79,4 +59,48 @@
@media (max-width: 567px) {
padding-bottom: 55px;
}
}
.chart.graph {
min-height: 600px;
}
.chart.nodepage {
min-height: 100%;
width: 100%;
height: 100%;
padding-bottom: 0px;
}
.chart.channelpage {
min-height: 400px;
}
.widget {
width: 100%;
margin-left: auto;
margin-right: auto;
height: 250px;
-webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%);
}
.widget > .chart {
min-height: 250px;
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
@media (max-width: 767.98px) {
padding-bottom: 0px;
}
}
.loading-spinner {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
.loading-spinner.widget {
position: absolute;
top: 200px;
z-index: 100;
width: 100%;
left: 0;
@media (max-width: 767.98px) {
top: 250px;
}
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.service';
import { Observable, switchMap, tap, zip } from 'rxjs';
@@ -16,14 +16,14 @@ import { isMobile } from 'src/app/shared/common.utils';
styleUrls: ['./nodes-channels-map.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodesChannelsMap implements OnInit, OnDestroy {
export class NodesChannelsMap implements OnInit {
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
@Input() publicKey: string | undefined;
@Input() channel: any[] = [];
@Input() fitContainer = false;
@Output() readyEvent = new EventEmitter();
observable$: Observable<any>;
channelsObservable: Observable<any>;
center: number[] | undefined;
zoom: number | undefined;
@@ -31,6 +31,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
channelOpacity = 0.1;
channelColor = '#466d9d';
channelCurve = 0;
nodeSize = 4;
isLoading = true;
chartInstance = undefined;
chartOptions: EChartsOption = {};
@@ -49,8 +51,6 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
) {
}
ngOnDestroy(): void {}
ngOnInit(): void {
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
this.zoom = 1.3;
@@ -65,13 +65,17 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
if (this.style === 'graph') {
this.seoService.setTitle($localize`Lightning nodes channels world map`);
}
if (['nodepage', 'channelpage'].includes(this.style)) {
this.nodeSize = 8;
}
this.observable$ = this.activatedRoute.paramMap
this.channelsObservable = this.activatedRoute.paramMap
.pipe(
switchMap((params: ParamMap) => {
return zip(
this.assetsService.getWorldMapJson$,
this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined),
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]);
@@ -90,10 +94,12 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
}
}
for (const channel of geoloc) {
if (!thisNodeGPS && data[2] === channel[0]) {
thisNodeGPS = [channel[2], channel[3]];
} else if (!thisNodeGPS && data[2] === channel[4]) {
thisNodeGPS = [channel[6], channel[7]];
if (this.style === 'nodepage' && !thisNodeGPS) {
if (data[2] === channel[0]) {
thisNodeGPS = [channel[2], channel[3]];
} else if (data[2] === channel[4]) {
thisNodeGPS = [channel[6], channel[7]];
}
}
// 0 - node1 pubkey
@@ -102,48 +108,68 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
// 4 - node2 pubkey
// 5 - node2 alias
// 6,7 - node2 GPS
const node1PubKey = 0;
const node1Alias = 1;
let node1GpsLat = 2;
let node1GpsLgt = 3;
const node2PubKey = 4;
const node2Alias = 5;
let node2GpsLat = 6;
let node2GpsLgt = 7;
let node1UniqueId = channel[node1PubKey];
let node2UniqueId = channel[node2PubKey];
if (this.style === 'widget') {
node1GpsLat = 0;
node1GpsLgt = 1;
node2GpsLat = 2;
node2GpsLgt = 3;
node1UniqueId = channel[node1GpsLat].toString() + channel[node1GpsLgt].toString();
node2UniqueId = channel[node2GpsLat].toString() + channel[node2GpsLgt].toString();
}
// We add a bit of noise so nodes at the same location are not all
// on top of each other
let random = Math.random() * 2 * Math.PI;
let random2 = Math.random() * 0.01;
if (!nodesPubkeys[channel[0]]) {
if (!nodesPubkeys[node1UniqueId]) {
nodes.push([
channel[2] + random2 * Math.cos(random),
channel[3] + random2 * Math.sin(random),
channel[node1GpsLat] + random2 * Math.cos(random),
channel[node1GpsLgt] + random2 * Math.sin(random),
1,
channel[0],
channel[1]
channel[node1PubKey],
channel[node1Alias]
]);
nodesPubkeys[channel[0]] = nodes[nodes.length - 1];
nodesPubkeys[node1UniqueId] = nodes[nodes.length - 1];
}
random = Math.random() * 2 * Math.PI;
random2 = Math.random() * 0.01;
if (!nodesPubkeys[channel[4]]) {
if (!nodesPubkeys[node2UniqueId]) {
nodes.push([
channel[6] + random2 * Math.cos(random),
channel[7] + random2 * Math.sin(random),
channel[node2GpsLat] + random2 * Math.cos(random),
channel[node2GpsLgt] + random2 * Math.sin(random),
1,
channel[4],
channel[5]
channel[node2PubKey],
channel[node2Alias]
]);
nodesPubkeys[channel[4]] = nodes[nodes.length - 1];
nodesPubkeys[node2UniqueId] = nodes[nodes.length - 1];
}
const channelLoc = [];
channelLoc.push(nodesPubkeys[channel[0]].slice(0, 2));
channelLoc.push(nodesPubkeys[channel[4]].slice(0, 2));
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
channelLoc.push(nodesPubkeys[node2UniqueId].slice(0, 2));
channelsLoc.push(channelLoc);
}
if (this.style === 'nodepage' && thisNodeGPS) {
this.center = [thisNodeGPS[0], thisNodeGPS[1]];
this.zoom = 10;
this.zoom = 5;
this.channelWidth = 1;
this.channelOpacity = 1;
}
if (this.style === 'channelpage' && this.channel.length > 0) {
this.channelWidth = 2;
this.channelOpacity = 1;
@@ -170,15 +196,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
prepareChartOptions(nodes, channels) {
let title: object;
if (channels.length === 0) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`No geolocation data available`,
left: 'center',
top: 'center'
};
this.chartOptions = null;
return;
}
this.chartOptions = {
@@ -214,7 +233,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
data: nodes,
coordinateSystem: 'geo',
geoIndex: 0,
symbolSize: 4,
symbolSize: this.nodeSize,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
@@ -242,7 +261,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
},
{
large: false,
progressive: 200,
progressive: this.style === 'widget' ? 500 : 200,
silent: true,
type: 'lines',
coordinateSystem: 'geo',
@@ -270,6 +289,10 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
this.chartInstance = ec;
this.chartInstance.on('finished', () => {
this.isLoading = false;
});
if (this.style === 'widget') {
this.chartInstance.getZr().on('click', (e) => {
this.zone.run(() => {

View File

@@ -0,0 +1,2 @@
<div *ngIf="channelsObservable$ | async" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>

View File

@@ -0,0 +1,138 @@
import { formatNumber } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
import { Observable, tap } from 'rxjs';
import { lerpColor } from 'src/app/shared/graphs.utils';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '../lightning-api.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-node-channels',
templateUrl: './node-channels.component.html',
styleUrls: ['./node-channels.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodeChannels implements OnChanges {
@Input() publicKey: string;
chartInstance: ECharts;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
channelsObservable$: Observable<any>;
isLoading: true;
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
private stateService: StateService,
) {}
ngOnChanges(): void {
this.prepareChartOptions(null);
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
.pipe(
tap((response) => {
const biggestCapacity = response.body[0].capacity;
this.prepareChartOptions(response.body.map(channel => {
return {
name: channel.node.alias,
value: channel.capacity,
shortId: channel.short_id,
id: channel.id,
itemStyle: {
color: lerpColor('#1E88E5', '#D81B60', Math.pow(channel.capacity / biggestCapacity, 0.4)),
}
};
}));
})
);
}
prepareChartOptions(data): void {
this.chartOptions = {
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: <TreemapSeriesOption[]>[
{
left: 0,
right: 0,
bottom: 0,
top: 0,
roam: false,
type: 'treemap',
data: data,
nodeClick: 'link',
progressive: 100,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: (value): string => {
if (value.data.name === undefined) {
return ``;
}
let capacity = '';
if (value.data.value > 100000000) {
capacity = formatNumber(Math.round(value.data.value / 100000000), this.locale, '1.2-2') + ' BTC';
} else {
capacity = <string>this.amountShortenerPipe.transform(value.data.value, 2) + ' sats';
}
return `
<b style="color: white; margin-left: 2px">${value.data.shortId}</b><br>
<span>Node: ${value.name}</span><br>
<span>Capacity: ${capacity}</span>
`;
}
},
itemStyle: {
borderColor: 'black',
borderWidth: 1,
},
breadcrumb: {
show: false,
}
}
]
};
}
onChartInit(ec: ECharts): void {
if (this.chartInstance !== undefined) {
return;
}
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
//@ts-ignore
if (!e.data.id) {
return;
}
this.zone.run(() => {
//@ts-ignore
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/channel/${e.data.id}`);
this.router.navigate([url]);
});
});
}
}

View File

@@ -10,6 +10,7 @@ import { download } from 'src/app/shared/graphs.utils';
import { SeoService } from 'src/app/services/seo.service';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { isMobile } from 'src/app/shared/common.utils';
@Component({
selector: 'app-nodes-networks-chart',
@@ -108,19 +109,19 @@ export class NodesNetworksChartComponent implements OnInit {
);
}
prepareChartOptions(data, maxYAxis) {
prepareChartOptions(data, maxYAxis): void {
let title: object;
if (data.tor_nodes.length === 0) {
if (!this.widget && data.tor_nodes.length === 0) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
text: $localize`Indexing in progess`,
left: 'center',
top: 'top',
top: 'center',
};
} else if (this.widget) {
} else if (data.tor_nodes.length > 0) {
title = {
textStyle: {
color: 'grey',
@@ -140,11 +141,11 @@ export class NodesNetworksChartComponent implements OnInit {
height: this.widget ? 100 : undefined,
top: this.widget ? 10 : 40,
bottom: this.widget ? 0 : 70,
right: (this.isMobile() && this.widget) ? 35 : this.right,
left: (this.isMobile() && this.widget) ? 40 :this.left,
right: (isMobile() && this.widget) ? 35 : this.right,
left: (isMobile() && this.widget) ? 40 :this.left,
},
tooltip: {
show: !this.isMobile() || !this.widget,
show: !isMobile() || !this.widget,
trigger: 'axis',
axisPointer: {
type: 'line'
@@ -157,7 +158,7 @@ export class NodesNetworksChartComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
formatter: (ticks): string => {
let total = 0;
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
@@ -180,7 +181,7 @@ export class NodesNetworksChartComponent implements OnInit {
},
xAxis: data.tor_nodes.length === 0 ? undefined : {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
splitNumber: (isMobile() || this.widget) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
@@ -372,7 +373,7 @@ export class NodesNetworksChartComponent implements OnInit {
};
}
onChartInit(ec) {
onChartInit(ec): void {
if (this.chartInstance !== undefined) {
return;
}
@@ -384,11 +385,7 @@ export class NodesNetworksChartComponent implements OnInit {
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
onSaveChart(): void {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();

View File

@@ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service';
import { download } from 'src/app/shared/graphs.utils';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
@Component({
selector: 'app-nodes-per-country-chart',

View File

@@ -36,7 +36,7 @@
{{ node.channels }}
</td>
<td class="city text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
</td>
</tbody>
</table>

View File

@@ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-nodes-per-country',
@@ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit {
name: response.country.en,
flag: getFlagEmoji(this.route.snapshot.params.country)
};
for (const i in response.nodes) {
response.nodes[i].geolocation = <GeolocationData>{
country: response.nodes[i].country?.en,
city: response.nodes[i].city?.en,
subdivision: response.nodes[i].subdivision?.en,
iso: response.nodes[i].iso_code,
};
}
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
return response.nodes;
})

View File

@@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
$localize`${isp[4].toString()} nodes<br>` +
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
;

View File

@@ -33,7 +33,7 @@
{{ node.channels }}
</td>
<td class="city text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tbody>
</table>

View File

@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-nodes-per-isp',
@@ -29,6 +30,16 @@ export class NodesPerISP implements OnInit {
id: this.route.snapshot.params.isp
};
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
for (const i in response.nodes) {
response.nodes[i].geolocation = <GeolocationData>{
country: response.nodes[i].country?.en,
city: response.nodes[i].city?.en,
subdivision: response.nodes[i].subdivision?.en,
iso: response.nodes[i].iso_code,
};
}
return response.nodes;
})
);

View File

@@ -1,6 +1,6 @@
<div [class]="!widget ? 'container-xl full-height' : ''">
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-capacity">
<span>Top 100 nodes by capacity</span>
<h1 *ngIf="!widget" class="float-left">
<span i18n="lightning.top-100-liquidity">Top 100 nodes liquidity ranking</span>
</h1>
<div [class]="widget ? 'widget' : 'full'">

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
import { isMobile } from 'src/app/shared/common.utils';
import { LightningApiService } from '../../lightning-api.service';
@Component({
@@ -19,7 +20,7 @@ export class TopNodesPerCapacity implements OnInit {
constructor(private apiService: LightningApiService) {}
ngOnInit(): void {
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i);
}
@@ -28,7 +29,7 @@ export class TopNodesPerCapacity implements OnInit {
} else {
this.topNodesPerCapacity$ = this.nodes$.pipe(
map((ranking) => {
return ranking.topByCapacity.slice(0, 10);
return ranking.topByCapacity.slice(0, isMobile() ? 8 : 7);
})
);
}

View File

@@ -1,6 +1,6 @@
<div [class]="!widget ? 'container-xl full-height' : ''">
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-channel">
<span>Top 100 nodes by channel count</span>
<h1 *ngIf="!widget" class="float-left">
<span i18n="lightning.top-100-connectivity">Top 100 nodes connectivity ranking</span>
</h1>
<div [class]="widget ? 'widget' : 'full'">

View File

@@ -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 { isMobile } from 'src/app/shared/common.utils';
import { LightningApiService } from '../../lightning-api.service';
@Component({
@@ -19,7 +20,7 @@ export class TopNodesPerChannels implements OnInit {
constructor(private apiService: LightningApiService) {}
ngOnInit(): void {
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i);
}
@@ -28,7 +29,7 @@ export class TopNodesPerChannels implements OnInit {
} else {
this.topNodesPerChannels$ = this.nodes$.pipe(
map((ranking) => {
return ranking.topByChannels.slice(0, 10);
return ranking.topByChannels.slice(0, isMobile() ? 8 : 7);
})
);
}

View File

@@ -4,8 +4,8 @@
<div class="col">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
@@ -18,8 +18,8 @@
<div class="col">
<div class="card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>

View File

@@ -48,4 +48,10 @@
<div class="spinner-border text-light"></div>
</div>
<div *ngIf="widget && (capacityObservable$ | async) as stats">
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
Indexing in progress
</div>
</div>
</div>

View File

@@ -131,4 +131,13 @@
display: block;
max-width: 80px;
margin: 15px auto 3px;
}
.indexing-message {
position: absolute;
font-size: 15px;
color: grey;
font-weight: bold;
margin-left: calc(50% - 85px);
margin-top: -10px;
}

View File

@@ -1,7 +1,7 @@
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs';
import { map, startWith, switchMap, tap } from 'rxjs/operators';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
@@ -10,6 +10,7 @@ import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { isMobile } from 'src/app/shared/common.utils';
@Component({
selector: 'app-lightning-statistics-chart',
@@ -96,12 +97,13 @@ export class LightningStatisticsChartComponent implements OnInit {
}),
);
}),
)
share(),
);
}
prepareChartOptions(data) {
prepareChartOptions(data): void {
let title: object;
if (data.channel_count.length === 0) {
if (!this.widget && data.channel_count.length === 0) {
title = {
textStyle: {
color: 'grey',
@@ -111,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
left: 'center',
top: 'center'
};
} else if (this.widget) {
} else if (data.channel_count.length > 0) {
title = {
textStyle: {
color: 'grey',
@@ -138,11 +140,11 @@ export class LightningStatisticsChartComponent implements OnInit {
height: this.widget ? 100 : undefined,
top: this.widget ? 10 : 40,
bottom: this.widget ? 0 : 70,
right: (this.isMobile() && this.widget) ? 35 : this.right,
left: (this.isMobile() && this.widget) ? 40 :this.left,
right: (isMobile() && this.widget) ? 35 : this.right,
left: (isMobile() && this.widget) ? 40 :this.left,
},
tooltip: {
show: !this.isMobile(),
show: !isMobile(),
trigger: 'axis',
axisPointer: {
type: 'line'
@@ -155,7 +157,7 @@ export class LightningStatisticsChartComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
formatter: (ticks): string => {
let sizeString = '';
let weightString = '';
@@ -169,16 +171,18 @@ export class LightningStatisticsChartComponent implements OnInit {
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>
const tooltip = `
<b style="color: white; margin-left: 18px">${date}</b><br>
<span>${sizeString}</span><br>
<span>${weightString}</span>`;
<span>${weightString}</span>
`;
return tooltip;
}
},
xAxis: data.channel_count.length === 0 ? undefined : {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
splitNumber: (isMobile() || this.widget) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
@@ -315,7 +319,7 @@ export class LightningStatisticsChartComponent implements OnInit {
};
}
onChartInit(ec) {
onChartInit(ec): void {
if (this.chartInstance !== undefined) {
return;
}
@@ -327,11 +331,7 @@ export class LightningStatisticsChartComponent implements OnInit {
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
onSaveChart(): void {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();

View File

@@ -271,10 +271,11 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
}
getChannelsGeo$(publicKey?: string): Observable<any> {
getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' +
(publicKey !== undefined ? `/${publicKey}` : '')
(publicKey !== undefined ? `/${publicKey}` : '') +
(style !== undefined ? `?style=${style}` : '')
);
}
}

View File

@@ -153,7 +153,7 @@ export class StateService {
if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
return;
}
const networkMatches = url.match(/\/(bisq|testnet|liquidtestnet|liquid|signet)/);
const networkMatches = url.match(/^\/(bisq|testnet|liquidtestnet|liquid|signet)/);
switch (networkMatches && networkMatches[1]) {
case 'liquid':
if (this.network !== 'liquid') {

View File

@@ -1,3 +1,120 @@
export function isMobile() {
export function isMobile(): boolean {
return (window.innerWidth <= 767.98);
}
export function getFlagEmoji(countryCode): string {
if (!countryCode) {
return '';
}
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
// https://gist.github.com/calebgrove/c285a9510948b633aa47
export function convertRegion(input, to: 'name' | 'abbreviated'): string {
if (!input) {
return '';
}
const states = [
['Alabama', 'AL'],
['Alaska', 'AK'],
['American Samoa', 'AS'],
['Arizona', 'AZ'],
['Arkansas', 'AR'],
['Armed Forces Americas', 'AA'],
['Armed Forces Europe', 'AE'],
['Armed Forces Pacific', 'AP'],
['California', 'CA'],
['Colorado', 'CO'],
['Connecticut', 'CT'],
['Delaware', 'DE'],
['District Of Columbia', 'DC'],
['Florida', 'FL'],
['Georgia', 'GA'],
['Guam', 'GU'],
['Hawaii', 'HI'],
['Idaho', 'ID'],
['Illinois', 'IL'],
['Indiana', 'IN'],
['Iowa', 'IA'],
['Kansas', 'KS'],
['Kentucky', 'KY'],
['Louisiana', 'LA'],
['Maine', 'ME'],
['Marshall Islands', 'MH'],
['Maryland', 'MD'],
['Massachusetts', 'MA'],
['Michigan', 'MI'],
['Minnesota', 'MN'],
['Mississippi', 'MS'],
['Missouri', 'MO'],
['Montana', 'MT'],
['Nebraska', 'NE'],
['Nevada', 'NV'],
['New Hampshire', 'NH'],
['New Jersey', 'NJ'],
['New Mexico', 'NM'],
['New York', 'NY'],
['North Carolina', 'NC'],
['North Dakota', 'ND'],
['Northern Mariana Islands', 'NP'],
['Ohio', 'OH'],
['Oklahoma', 'OK'],
['Oregon', 'OR'],
['Pennsylvania', 'PA'],
['Puerto Rico', 'PR'],
['Rhode Island', 'RI'],
['South Carolina', 'SC'],
['South Dakota', 'SD'],
['Tennessee', 'TN'],
['Texas', 'TX'],
['US Virgin Islands', 'VI'],
['Utah', 'UT'],
['Vermont', 'VT'],
['Virginia', 'VA'],
['Washington', 'WA'],
['West Virginia', 'WV'],
['Wisconsin', 'WI'],
['Wyoming', 'WY'],
];
// So happy that Canada and the US have distinct abbreviations
const provinces = [
['Alberta', 'AB'],
['British Columbia', 'BC'],
['Manitoba', 'MB'],
['New Brunswick', 'NB'],
['Newfoundland', 'NF'],
['Northwest Territory', 'NT'],
['Nova Scotia', 'NS'],
['Nunavut', 'NU'],
['Ontario', 'ON'],
['Prince Edward Island', 'PE'],
['Quebec', 'QC'],
['Saskatchewan', 'SK'],
['Yukon', 'YT'],
];
const regions = states.concat(provinces);
let i; // Reusable loop variable
if (to == 'abbreviated') {
input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
for (i = 0; i < regions.length; i++) {
if (regions[i][0] == input) {
return (regions[i][1]);
}
}
} else if (to == 'name') {
input = input.toUpperCase();
for (i = 0; i < regions.length; i++) {
if (regions[i][1] == input) {
return (regions[i][0]);
}
}
}
}

View File

@@ -0,0 +1 @@
<span [innerHTML]="formattedLocation"></span>

View File

@@ -0,0 +1,83 @@
import { Component, Input, OnChanges } from '@angular/core';
import { convertRegion, getFlagEmoji } from '../../common.utils';
export interface GeolocationData {
country: string;
city: string;
subdivision: string;
iso: string;
}
@Component({
selector: 'app-geolocation',
templateUrl: './geolocation.component.html',
styleUrls: ['./geolocation.component.scss']
})
export class GeolocationComponent implements OnChanges {
@Input() data: GeolocationData;
@Input() type: 'node' | 'list-isp' | 'list-country';
formattedLocation: string = '';
ngOnChanges(): void {
const city = this.data.city ? this.data.city : '';
const subdivisionLikeCity = this.data.city === this.data.subdivision;
let subdivision = this.data.subdivision;
if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) {
this.data.subdivision = undefined;
} else if (['list-isp', 'list-country'].includes(this.type) === true) {
subdivision = convertRegion(this.data.subdivision, 'abbreviated');
}
if (this.type === 'list-country') {
if (this.data.city) {
this.formattedLocation += ' ' + city;
if (this.data.subdivision) {
this.formattedLocation += ', ' + subdivision;
}
} else {
this.formattedLocation += '-';
}
}
if (this.type === 'list-isp') {
this.formattedLocation = getFlagEmoji(this.data.iso);
if (this.data.city) {
this.formattedLocation += ' ' + city;
if (this.data.subdivision) {
this.formattedLocation += ', ' + subdivision;
}
} else {
this.formattedLocation += ' ' + this.data.country;
}
}
if (this.type === 'node') {
const city = this.data.city ? this.data.city : '';
// City
this.formattedLocation = `${city}`;
// ,Subdivision
if (this.formattedLocation.length > 0 && !subdivisionLikeCity) {
this.formattedLocation += ', ';
}
if (!subdivisionLikeCity) {
this.formattedLocation += `${subdivision}`;
}
// <br>[flag] County
if (this.data?.country.length ?? 0 > 0) {
if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) {
this.formattedLocation += '<br>';
} else if (this.data.city) {
this.formattedLocation += ', ';
}
this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
}
return;
}
}
}

View File

@@ -91,13 +91,25 @@ export function detectWebGL() {
return (gl && gl instanceof WebGLRenderingContext);
}
export function getFlagEmoji(countryCode) {
if (!countryCode) {
return '';
}
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
/**
* https://gist.githubusercontent.com/rosszurowski/67f04465c424a9bc0dae/raw/90ee06c5aa84ab352eb5b233d0a8263c3d8708e5/lerp-color.js
* A linear interpolator for hexadecimal colors
* @param {String} a
* @param {String} b
* @param {Number} amount
* @example
* // returns #7F7F7F
* lerpColor('#000000', '#ffffff', 0.5)
* @returns {String}
*/
export function lerpColor(a: string, b: string, amount: number): string {
const ah = parseInt(a.replace(/#/g, ''), 16),
ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
bh = parseInt(b.replace(/#/g, ''), 16),
br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
rr = ar + amount * (br - ar),
rg = ag + amount * (bg - ag),
rb = ab + amount * (bb - ab);
return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
}

View File

@@ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component';
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
import { TimestampComponent } from './components/timestamp/timestamp.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
@NgModule({
declarations: [
@@ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
SearchResultsComponent,
TimestampComponent,
ToggleComponent,
GeolocationComponent,
],
imports: [
CommonModule,
@@ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
SearchResultsComponent,
TimestampComponent,
ToggleComponent,
GeolocationComponent,
]
})
export class SharedModule {