Merge pull request #3935 from mempool/mononaut/lightning-justice

Add lightning justice page
This commit is contained in:
softsimon 2023-07-18 17:24:26 +09:00 committed by GitHub
commit 7046c3d6c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 252 additions and 3 deletions

View file

@ -117,6 +117,26 @@ class ChannelsApi {
}
}
public async $getPenaltyClosedChannels(): Promise<any[]> {
try {
const query = `
SELECT n1.alias AS alias_left,
n2.alias AS alias_right,
channels.*
FROM channels
LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key
LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key
WHERE channels.status = 2 AND channels.closing_reason = 3
ORDER BY closing_date DESC
`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getPenaltyClosedChannels error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getUnresolvedClosedChannels(): Promise<any[]> {
try {
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason = 2 AND closing_resolved = 0 AND closing_transaction_id != ''`;

View file

@ -11,6 +11,7 @@ class ChannelsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/penalties', this.$getPenaltyClosedChannels)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getAllChannelsGeo)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo/:publicKey', this.$getAllChannelsGeo)
;
@ -108,6 +109,18 @@ class ChannelsRoutes {
}
}
private async $getPenaltyClosedChannels(req: Request, res: Response): Promise<void> {
try {
const channels = await channelsApi.$getPenaltyClosedChannels();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getAllChannelsGeo(req: Request, res: Response) {
try {
const style: string = typeof req.query.style === 'string' ? req.query.style : '';

View file

@ -73,7 +73,7 @@
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
<div>
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
</div>
</ng-template>
</ng-container>

View file

@ -270,6 +270,7 @@ export interface IChannel {
closing_transaction_id: string;
closing_reason: string;
updated_at: string;
closing_date?: string;
created: string;
status: number;
node_left: INode,

View file

@ -0,0 +1,83 @@
<div class="container-xl full-height" style="min-height: 335px">
<h1 class="float-left" i18n="lightning.liquidity-ranking">Penalties</h1>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead>
<th class="timestamp" i18n="lightning.closed-at">Closed</th>
<th class="channels text-right" i18n="lightning.capacity">Capacity</th>
<th class="node text-right"></th>
<th class="node text-right" i18n="lightning.node">Nodes</th>
<th class="channelid text-right" i18n="channels.id">Channel ID</th>
<th></th>
</thead>
<tbody *ngIf="justiceChannels$ | async as channels">
<ng-container *ngFor="let channel of channels;">
<tr>
<td class="timestamp">
&lrm;{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td class="capacity text-right">
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallnode>
{{ channel.capacity | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
</td>
<td class="alias text-right">
<app-truncate [text]="channel.alias_left || '?'" [maxWidth]="200" [lastChars]="6" textAlign="end" [inline]="true"></app-truncate>
</td>
<td class="alias text-right">
<app-truncate [text]="channel.alias_right || '?'" [maxWidth]="200" [lastChars]="6" textAlign="end" [inline]="true"></app-truncate>
</td>
<td class="channelid text-right">
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
</td>
<td class="text-right">
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="toggleDetails(channel)"
i18n="transaction.details|Transaction Details">Details</button>
</td>
</tr>
<tr *ngIf="channel.short_id === expanded">
<ng-container *ngTemplateOutlet="channelTransactions"></ng-container>
</tr>
</ng-container>
</tbody>
</table>
<div class="clearfix"></div>
<br>
</div>
</div>
<ng-template #channelTransactions>
<td colspan="6" *ngIf="transactions && !loadingTransactions else loadingTemplate;">
<ng-template [ngIf]="transactions[0]">
<div class="d-flex">
<h5 i18n="lightning.opening-transaction">Opening transaction</h5>
</div>
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
</app-transactions-list>
</ng-template>
<ng-template [ngIf]="transactions[1]">
<div class="closing-header d-flex">
<h5 style="margin: 0;" i18n="lightning.closing-transaction">Closing transaction</h5>&nbsp;&nbsp;<app-closing-type [type]="3"></app-closing-type>
</div>
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
</app-transactions-list>
</ng-template>
</td>
</ng-template>
<ng-template #loadingTemplate>
<td colspan="6">
<div class="text-center">
<div class="spinner-border text-light"></div>
</div>
</td>
</ng-template>

View file

@ -0,0 +1,52 @@
.container-xl {
max-width: 1400px;
}
.container-xl.widget {
padding-right: 0px;
padding-left: 0px;
padding-bottom: 0px;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
.clear-link {
color: white;
}
.pool {
width: 15%;
@media (max-width: 575px) {
width: 75%;
}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.pool-name {
display: inline-block;
vertical-align: text-top;
text-overflow: ellipsis;
overflow: hidden;
}
.liquidity {
width: 10%;
@media (max-width: 575px) {
width: 25%;
}
}
.fiat {
width: 15%;
@media (min-width: 768px) and (max-width: 991px) {
display: none !important;
}
@media (max-width: 575px) {
display: none !important;
}
}

View file

@ -0,0 +1,60 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { map, Observable, of, Subject, Subscription, switchMap, tap, zip } from 'rxjs';
import { IChannel } from '../../interfaces/node-api.interface';
import { LightningApiService } from '../lightning-api.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
@Component({
selector: 'app-justice-list',
templateUrl: './justice-list.component.html',
styleUrls: ['./justice-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JusticeList implements OnInit, OnDestroy {
justiceChannels$: Observable<any[]>;
fetchTransactions$: Subject<IChannel> = new Subject();
transactionsSubscription: Subscription;
transactions: Transaction[];
expanded: string = null;
loadingTransactions: boolean = true;
constructor(
private apiService: LightningApiService,
private electrsApiService: ElectrsApiService,
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.justiceChannels$ = this.apiService.getPenaltyClosedChannels$();
this.transactionsSubscription = this.fetchTransactions$.pipe(
tap(() => {
this.loadingTransactions = true;
}),
switchMap((channel: IChannel) => {
return zip([
channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id) : of(null),
]);
}),
).subscribe((transactions) => {
this.transactions = transactions;
this.loadingTransactions = false;
this.cd.markForCheck();
});
}
toggleDetails(channel: any): void {
if (this.expanded === channel.short_id) {
this.expanded = null;
} else {
this.expanded = channel.short_id;
this.fetchTransactions$.next(channel);
}
}
ngOnDestroy(): void {
this.transactionsSubscription.unsubscribe();
}
}

View file

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { StateService } from '../services/state.service';
import { INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
@Injectable({
providedIn: 'root'
@ -84,6 +84,12 @@ export class LightningApiService {
);
}
getPenaltyClosedChannels$(): Observable<IChannel[]> {
return this.httpClient.get<IChannel[]>(
this.apiBasePath + '/api/v1/lightning/penalties'
);
}
getOldestNodes$(): Observable<IOldestNodes[]> {
return this.httpClient.get<IOldestNodes[]>(
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'

View file

@ -29,6 +29,7 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component';
import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component';
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
import { JusticeList } from '../lightning/justice-list/justice-list.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';
@ -60,6 +61,7 @@ import { GroupComponent } from './group/group.component';
NodesRanking,
TopNodesPerChannels,
TopNodesPerCapacity,
JusticeList,
OldestNodes,
NodesRankingsDashboard,
NodeChannels,
@ -97,6 +99,7 @@ import { GroupComponent } from './group/group.component';
NodesRanking,
TopNodesPerChannels,
TopNodesPerCapacity,
JusticeList,
OldestNodes,
NodesRankingsDashboard,
NodeChannels,

View file

@ -9,6 +9,7 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
import { NodesRanking } from './nodes-ranking/nodes-ranking.component';
import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component';
import { GroupComponent } from './group/group.component';
import { JusticeList } from './justice-list/justice-list.component';
const routes: Routes = [
{
@ -66,6 +67,10 @@ const routes: Routes = [
type: 'oldest'
},
},
{
path: 'penalties',
component: JusticeList,
},
{
path: '**',
redirectTo: ''

View file

@ -1,4 +1,4 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null">
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>

View file

@ -23,4 +23,8 @@
flex-shrink: 0;
flex-grow: 0;
}
&.inline {
display: inline-flex;
}
}

View file

@ -11,6 +11,8 @@ export class TruncateComponent {
@Input() link: any = null;
@Input() lastChars: number = 4;
@Input() maxWidth: number = null;
@Input() inline: boolean = false;
@Input() textAlign: 'start' | 'end' = 'start';
rtl: boolean;
constructor(