Merge branch 'master' into nymkappa/bugfix/db-deadlock

This commit is contained in:
wiz 2022-08-24 00:08:17 +09:00 committed by GitHub
commit 23fab65216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 265 additions and 187 deletions

View File

@ -245,8 +245,12 @@ class ChannelsApi {
let channelStatusFilter; let channelStatusFilter;
if (status === 'open') { if (status === 'open') {
channelStatusFilter = '< 2'; channelStatusFilter = '< 2';
} else if (status === 'active') {
channelStatusFilter = '= 1';
} else if (status === 'closed') { } else if (status === 'closed') {
channelStatusFilter = '= 2'; channelStatusFilter = '= 2';
} else {
throw new Error('getChannelsForNode: Invalid status requested');
} }
// Channels originating from node // Channels originating from node
@ -275,7 +279,12 @@ class ChannelsApi {
allChannels.sort((a, b) => { allChannels.sort((a, b) => {
return b.capacity - a.capacity; return b.capacity - a.capacity;
}); });
if (index >= 0) {
allChannels = allChannels.slice(index, index + length); allChannels = allChannels.slice(index, index + length);
} else if (index === -1) { // Node channels tree chart
allChannels = allChannels.slice(0, 1000);
}
const channels: any[] = [] const channels: any[] = []
for (const row of allChannels) { for (const row of allChannels) {

View File

@ -285,11 +285,24 @@ class NetworkSyncService {
for (const channel of channels) { for (const channel of channels) {
let reason = 0; let reason = 0;
// Only Esplora backend can retrieve spent transaction outputs // Only Esplora backend can retrieve spent transaction outputs
const outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id); try {
let outspends: IEsploraApi.Outspend[] | undefined;
try {
outspends = await bitcoinApi.$getOutspends(channel.closing_transaction_id);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const lightningScriptReasons: number[] = []; const lightningScriptReasons: number[] = [];
for (const outspend of outspends) { for (const outspend of outspends) {
if (outspend.spent && outspend.txid) { if (outspend.spent && outspend.txid) {
const spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid); let spendingTx: IEsploraApi.Transaction | undefined;
try {
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
lightningScriptReasons.push(lightningScript); lightningScriptReasons.push(lightningScript);
} }
@ -310,7 +323,13 @@ class NetworkSyncService {
We can detect a commitment transaction (force close) by reading Sequence and Locktime We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/ */
const closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id); let closingTx: IEsploraApi.Transaction | undefined;
try {
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
const sequenceHex: string = closingTx.vin[0].sequence.toString(16); const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
const locktimeHex: string = closingTx.locktime.toString(16); const locktimeHex: string = closingTx.locktime.toString(16);
if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') { if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') {
@ -324,6 +343,9 @@ class NetworkSyncService {
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.'); logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]); await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
} }
} catch (e) {
logger.err(`$runClosedChannelsForensics() failed for channel ${channel.short_id}. Reason: ${e instanceof Error ? e.message : e}`);
}
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);

View File

@ -87,7 +87,7 @@
</ng-template> </ng-template>
<ng-template #skeleton> <ng-template #skeleton>
<h2 class="float-left">Channels</h2> <h2 class="float-left" i18n="lightning.channels">Channels</h2>
<table class="table table-borderless"> <table class="table table-borderless">
<ng-container *ngTemplateOutlet="tableHeader"></ng-container> <ng-container *ngTemplateOutlet="tableHeader"></ng-container>

View File

@ -32,7 +32,7 @@ export class LightningApiService {
} }
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> { getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
let params = new HttpParams() const params = new HttpParams()
.set('public_key', publicKey) .set('public_key', publicKey)
.set('index', index) .set('index', index)
.set('status', status) .set('status', status)

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 { 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 { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
TopNodesPerCapacity, TopNodesPerCapacity,
OldestNodes, OldestNodes,
NodesRankingsDashboard, NodesRankingsDashboard,
NodeChannels,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
TopNodesPerCapacity, TopNodesPerCapacity,
OldestNodes, OldestNodes,
NodesRankingsDashboard, NodesRankingsDashboard,
NodeChannels,
], ],
providers: [ providers: [
LightningApiService, LightningApiService,

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 { .full-container {
padding: 0px 15px; margin-top: 25px;
width: 100%; margin-bottom: 25px;
/* min-height: 500px; */ min-height: 100%;
height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
};
}
/*
.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 { export class NodeStatisticsChartComponent implements OnInit {
@Input() publicKey: string; @Input() publicKey: string;
@Input() right: number | string = 65; @Input() right: number | string = 65;
@Input() left: number | string = 55; @Input() left: number | string = 45;
@Input() widget = false; @Input() widget = false;
miningWindowPreference: string; miningWindowPreference: string;
@ -96,7 +96,7 @@ export class NodeStatisticsChartComponent implements OnInit {
], ],
grid: { grid: {
top: 30, top: 30,
bottom: 70, bottom: 20,
right: this.right, right: this.right,
left: this.left, left: this.left,
}, },

View File

@ -118,15 +118,26 @@
</button> </button>
</div> </div>
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map> <div *ngIf="!error">
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart> <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 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> <h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
</div> </div>
<app-channels-list *ngIf="!error" [publicKey]="node.public_key" <app-channels-list [publicKey]="node.public_key"
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list> (channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
</div>
</div> </div>

View File

@ -11,7 +11,6 @@
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)" <div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
(chartFinished)="onChartFinished($event)"> (chartFinished)="onChartFinished($event)">
</div> </div>
</div>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div> <div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div> </div>

View File

@ -12,11 +12,6 @@
width: 100%; width: 100%;
min-height: 600px; min-height: 600px;
height: calc(100% - 150px); height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
}
} }
.full-container.nodepage { .full-container.nodepage {
min-height: 400px; min-height: 400px;
@ -27,6 +22,7 @@
min-height: 400px; min-height: 400px;
margin-top: 25px; margin-top: 25px;
margin-bottom: 25px; margin-bottom: 25px;
min-height: 100%;
} }
.full-container.widget { .full-container.widget {
height: 250px; height: 250px;
@ -68,21 +64,21 @@
min-height: 600px; min-height: 600px;
} }
.chart.nodepage { .chart.nodepage {
min-height: 400px; min-height: 100%;
width: 100%;
height: 100%;
padding-bottom: 0px;
} }
.chart.channelpage { .chart.channelpage {
min-height: 400px; min-height: 400px;
} }
.widget { .widget {
width: 90vw; width: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
height: 250px; height: 250px;
-webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%); -webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%);
@media (max-width: 767.98px) {
width: 100vw;
}
} }
.widget > .chart { .widget > .chart {
min-height: 250px; min-height: 250px;

View File

@ -165,7 +165,7 @@ export class NodesChannelsMap implements OnInit {
if (this.style === 'nodepage' && thisNodeGPS) { if (this.style === 'nodepage' && thisNodeGPS) {
this.center = [thisNodeGPS[0], thisNodeGPS[1]]; this.center = [thisNodeGPS[0], thisNodeGPS[1]];
this.zoom = 10; this.zoom = 5;
this.channelWidth = 1; this.channelWidth = 1;
this.channelOpacity = 1; this.channelOpacity = 1;
} }

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

@ -91,3 +91,25 @@ export function detectWebGL() {
return (gl && gl instanceof WebGLRenderingContext); return (gl && gl instanceof WebGLRenderingContext);
} }
/**
* 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);
}