Merge branch 'master' into simon/custom-lazy-loading-strategy

This commit is contained in:
wiz 2022-08-28 17:25:26 +02:00 committed by GitHub
commit 7e1ab55c01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 584 additions and 214 deletions

View File

@ -74,7 +74,7 @@ class Logger {
private getNetwork(): string { private getNetwork(): string {
if (config.LIGHTNING.ENABLED) { if (config.LIGHTNING.ENABLED) {
return 'lightning'; return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
} }
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {
return 'bisq'; return 'bisq';

View File

@ -20,6 +20,10 @@ class LightningStatsImporter {
logger.info('Caching funding txs for currently existing channels'); logger.info('Caching funding txs for currently existing channels');
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
return;
}
await this.$importHistoricalLightningStats(); await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot(); await this.$cleanupIncorrectSnapshot();
} }

View File

@ -2,6 +2,9 @@
<div class="page-title"> <div class="page-title">
<h1 i18n="shared.transaction">Transaction</h1> <h1 i18n="shared.transaction">Transaction</h1>
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]">
<span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span>
</a>
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features"> <div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
<app-tx-features [tx]="tx"></app-tx-features> <app-tx-features [tx]="tx"></app-tx-features>
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1"> <span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
@ -13,104 +16,50 @@
</div> </div>
</div> </div>
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link"> <div class="top-data row">
{{ txId }} <span class="field col-sm-4 text-left">
</a> <ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
<ng-template #defaultAmount>
<app-amount [satoshis]="totalValue"></app-amount>
</ng-template>
</span>
<span class="field col-sm-4 text-center">&lrm;{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }}</span>
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
</div>
<div class="row"> <div class="row graph-wrapper">
<div class="col-sm"> <tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
<table class="table table-borderless table-striped"> <div class="above-bow">
<tbody> <p class="field pair">
<tr *ngIf="tx.status.confirmed; else firstSeen"> <span [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></span>
<td i18n="block.timestamp">Timestamp</td> <span [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></span>
<td> </p>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} <p class="field" *ngIf="!isCoinbase(tx)">
</td> {{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</tr> </p>
<ng-template #firstSeen>
<tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td *ngIf="transactionTime > 0; else notSeen">
&lrm;{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td>
<ng-template #notSeen>
<td>?</td>
</ng-template>
</tr>
</ng-template>
<tr>
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
<td>
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
<ng-template #defaultAmount>
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
</ng-template>
</td>
</tr>
<tr>
<td i18n="block.size">Size</td>
<td [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.inputs">Inputs</td>
<td *ngIf="!isCoinbase(tx); else coinbaseInputs">{{ tx.vin.length }}</td>
<ng-template #coinbaseInputs>
<td i18n="transactions-list.coinbase">Coinbase</td>
</ng-template>
</tr>
</tbody>
</table>
</div> </div>
<div class="overlaid">
<div class="col-sm"> <ng-container [ngSwitch]="extraData">
<table class="table table-borderless table-striped"> <table class="opreturns" *ngSwitchCase="'coinbase'">
<tbody> <tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr>
<tr *ngIf="!cpfpInfo || (!cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length); else cpfpFee">
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
&nbsp;
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</td>
</tr>
<ng-template #cpfpFee>
<tr> <tr>
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> <td class="label">Coinbase</td>
<td> <td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
<div class="effective-fee-container">
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed">
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
</ng-template>
</div>
</td>
</tr> </tr>
</ng-template> </tbody>
<tr> </table>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> <table class="opreturns" *ngSwitchCase="'opreturn'">
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td> <tbody>
</tr> <ng-container *ngFor="let vout of opReturns.slice(0,3)">
<tr> <tr>
<td i18n="transaction.locktime">Locktime</td> <td class="label">OP_RETURN</td>
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td> <td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
</tr> </tr>
<tr> </ng-container>
<td i18n="transaction.outputs">Outputs</td> </tbody>
<td>{{ tx.vout.length }}</td> </table>
</tr> </ng-container>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,26 +10,10 @@
font-size: 28px; font-size: 28px;
} }
.btn-small-height {
line-height: 1.1;
}
.arrow-green {
color: #1a9436;
}
.arrow-red {
color: #dc3545;
}
.row { .row {
flex-direction: row; flex-direction: row;
} }
.effective-fee-container {
display: inline-block;
}
.title { .title {
h2 { h2 {
line-height: 1; line-height: 1;
@ -46,8 +30,9 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: baseline;
margin-bottom: 10px; margin-bottom: 2px;
max-width: 100%;
h1 { h1 {
font-size: 52px; font-size: 52px;
@ -58,6 +43,43 @@
.features { .features {
font-size: 24px; font-size: 24px;
} }
& > * {
flex-grow: 0;
flex-shrink: 0;
}
.tx-link {
flex-grow: 1;
flex-shrink: 1;
margin: 0 1em;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: baseline;
.truncated {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -2px;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}
.features {
align-self: center;
}
}
.top-data {
font-size: 28px;
} }
.table { .table {
@ -68,8 +90,76 @@
} }
} }
.field {
font-size: 32px;
margin: 0;
::ng-deep .symbol {
font-size: 24px;
}
.label {
color: #ffffff66;
}
&.pair > *:first-child {
margin-right: 1em;
}
}
.tx-link { .tx-link {
display: inline-block; display: inline;
font-size: 28px; font-size: 28px;
margin-bottom: 6px; margin-bottom: 6px;
} }
.graph-wrapper {
position: relative;
background: #181b2d;
padding: 10px;
padding-bottom: 0;
.above-bow {
position: absolute;
top: 20px;
left: 0;
right: 0;
margin: auto;
text-align: center;
}
.overlaid {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
text-align: left;
font-size: 28px;
max-width: 90%;
margin: auto;
overflow: hidden;
.opreturns {
width: auto;
margin: auto;
table-layout: auto;
background: #2d3348af;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
td {
padding: 10px 10px;
&.message {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
}
}
}
}

View File

@ -7,10 +7,9 @@ import {
catchError, catchError,
retryWhen, retryWhen,
delay, delay,
map
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { OpenGraphService } from 'src/app/services/opengraph.service'; import { OpenGraphService } from 'src/app/services/opengraph.service';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
@ -37,6 +36,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
showCpfpDetails = false; showCpfpDetails = false;
fetchCpfp$ = new Subject<string>(); fetchCpfp$ = new Subject<string>();
liquidUnblinding = new LiquidUnblinding(); liquidUnblinding = new LiquidUnblinding();
isLiquid = false;
totalValue: number;
opReturns: Vout[];
extraData: 'none' | 'coinbase' | 'opreturn';
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -49,7 +52,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.stateService.networkChanged$.subscribe( this.stateService.networkChanged$.subscribe(
(network) => (this.network = network) (network) => {
this.network = network;
if (this.network === 'liquid' || this.network == 'liquidtestnet') {
this.isLiquid = true;
}
}
); );
this.fetchCpfpSubscription = this.fetchCpfp$ this.fetchCpfpSubscription = this.fetchCpfp$
@ -152,6 +160,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.tx.feePerVsize = tx.fee / (tx.weight / 4); this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false; this.isLoadingTx = false;
this.error = undefined; this.error = undefined;
this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
this.opReturns = this.getOpReturns(this.tx);
this.extraData = this.chooseExtraData();
if (!tx.status.confirmed && tx.firstSeen) { if (!tx.status.confirmed && tx.firstSeen) {
this.transactionTime = tx.firstSeen; this.transactionTime = tx.firstSeen;
@ -217,6 +228,20 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b); return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
} }
getOpReturns(tx: Transaction): Vout[] {
return tx.vout.filter((v) => v.scriptpubkey_type === 'op_return' && v.scriptpubkey_asm !== 'OP_RETURN');
}
chooseExtraData(): 'none' | 'opreturn' | 'coinbase' {
if (this.isCoinbase(this.tx)) {
return 'coinbase';
} else if (this.opReturns?.length) {
return 'opreturn';
} else {
return 'none';
}
}
ngOnDestroy() { ngOnDestroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe();

View File

@ -0,0 +1,44 @@
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
<defs>
<marker id="input-arrow" viewBox="-5 -5 10 10"
refX="0" refY="0"
markerUnits="strokeWidth"
markerWidth="1.5" markerHeight="1"
orient="auto">
<path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
</marker>
<marker id="output-arrow" viewBox="-5 -5 10 10"
refX="0" refY="0"
markerUnits="strokeWidth"
markerWidth="1.5" markerHeight="1"
orient="auto">
<path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
</marker>
<marker id="fee-arrow" viewBox="-5 -5 10 10"
refX="0" refY="0"
markerUnits="strokeWidth"
markerWidth="1.5" markerHeight="1"
orient="auto">
</marker>
<linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" [attr.stop-color]="gradient[1]" />
</linearGradient>
<linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[1]" />
<stop offset="100%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[1]" />
<stop offset="50%" [attr.stop-color]="gradient[1]" />
<stop offset="100%" stop-color="transparent" />
</linearGradient>
</defs>
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
<ng-container *ngFor="let input of inputs">
<path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/>
</ng-container>
<ng-container *ngFor="let output of outputs">
<path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" />
</ng-container>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,15 @@
.bowtie {
.line {
fill: none;
&.input {
stroke: url(#input-gradient);
}
&.output {
stroke: url(#output-gradient);
}
&.fee {
stroke: url(#fee-gradient);
}
}
}

View File

@ -0,0 +1,169 @@
import { Component, OnInit, Input, OnChanges } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface';
interface SvgLine {
path: string;
style: string;
class?: string;
}
@Component({
selector: 'tx-bowtie-graph',
templateUrl: './tx-bowtie-graph.component.html',
styleUrls: ['./tx-bowtie-graph.component.scss'],
})
export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() network: string;
@Input() width = 1200;
@Input() height = 600;
@Input() combinedWeight = 100;
@Input() minWeight = 2; //
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
inputs: SvgLine[];
outputs: SvgLine[];
middle: SvgLine;
isLiquid: boolean = false;
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
// liquid: ['#116761', '#183550'],
liquid: ['#09a197', '#0f62af'],
// 'liquidtestnet': ['#494a4a', '#272e46'],
'liquidtestnet': ['#d2d2d2', '#979797'],
// testnet: ['#1d486f', '#183550'],
testnet: ['#4edf77', '#10a0af'],
// signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2'],
};
gradient: string[] = ['#105fb0', '#105fb0'];
ngOnInit(): void {
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
this.gradient = this.gradientColors[this.network];
this.initGraph();
}
ngOnChanges(): void {
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
this.gradient = this.gradientColors[this.network];
this.initGraph();
}
initGraph(): void {
const totalValue = this.calcTotalValue(this.tx);
const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; });
if (this.tx.fee && !this.isLiquid) {
voutWithFee.unshift({ type: 'fee', value: this.tx.fee });
}
this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands);
this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
this.middle = {
path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`,
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
};
}
calcTotalValue(tx: Transaction): number {
const totalOutput = this.tx.vout.reduce((acc, v) => (v.value == null ? 0 : v.value) + acc, 0);
// simple sum of outputs + fee for bitcoin
if (!this.isLiquid) {
return this.tx.fee ? totalOutput + this.tx.fee : totalOutput;
} else {
const totalInput = this.tx.vin.reduce((acc, v) => (v?.prevout?.value == null ? 0 : v.prevout.value) + acc, 0);
const confidentialInputCount = this.tx.vin.reduce((acc, v) => acc + (v?.prevout?.value == null ? 1 : 0), 0);
const confidentialOutputCount = this.tx.vout.reduce((acc, v) => acc + (v.value == null ? 1 : 0), 0);
// if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
if (confidentialInputCount && confidentialOutputCount) {
const knownInputCount = (tx.vin.length - confidentialInputCount) || 1;
const knownOutputCount = (tx.vout.length - confidentialOutputCount) || 1;
// assume confidential inputs/outputs have the same average value as the known ones
const adjustedTotalInput = totalInput + ((totalInput / knownInputCount) * confidentialInputCount);
const adjustedTotalOutput = totalOutput + ((totalOutput / knownOutputCount) * confidentialOutputCount);
return Math.max(adjustedTotalInput, adjustedTotalOutput) || 1;
} else {
// otherwise knowing the actual total of one side suffices
return Math.max(totalInput, totalOutput) || 1;
}
}
}
initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] {
const lines = [];
let unknownCount = 0;
let unknownTotal = total == null ? this.combinedWeight : total;
xputs.forEach(put => {
if (put.value == null) {
unknownCount++;
} else {
unknownTotal -= put.value as number;
}
});
const unknownShare = unknownTotal / unknownCount;
// conceptual weights
const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
// actual displayed line thicknesses
const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
const gaps = visibleStrands - 1;
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
const innerBottom = innerTop + this.combinedWeight;
// tracks the visual bottom of the endpoints of the previous line
let lastOuter = 0;
let lastInner = innerTop;
// gap between strands
const spacing = (this.height - visibleWeight) / gaps;
for (let i = 0; i < xputs.length; i++) {
const weight = weights[i];
const minWeight = minWeights[i];
// set the vertical position of the (center of the) outer side of the line
let outer = lastOuter + (minWeight / 2);
const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
// special case to center single input/outputs
if (xputs.length === 1) {
outer = (this.height / 2);
}
lastOuter += minWeight + spacing;
lastInner += weight;
lines.push({
path: this.makePath(side, outer, inner, minWeight),
style: this.makeStyle(minWeight, xputs[i].type),
class: xputs[i].type
});
}
return lines;
}
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
const center = this.width / 2 + (side === 'in' ? -45 : 45 );
const midpoint = (start + center) / 2;
// correct for svg horizontal gradient bug
if (Math.round(outer) === Math.round(inner)) {
outer -= 1;
}
return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
}
makeStyle(minWeight, type): string {
if (type === 'fee') {
return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`;
} else {
return `stroke-width: ${minWeight}`;
}
}
}

View File

@ -4,7 +4,7 @@
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" > <a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
{{ channel.public_key | shortenString : 12 }} {{ channel.public_key | shortenString : 12 }}
</a> </a>
<app-clipboard [text]="channel.node1_public_key"></app-clipboard> <app-clipboard [text]="channel.public_key"></app-clipboard>
</div> </div>
<div class="box-right"> <div class="box-right">
<div class="second-line">{{ channel.channels }} channels</div> <div class="second-line">{{ channel.channels }} channels</div>
@ -51,4 +51,4 @@
</div> </div>
</div> </div>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>

View File

@ -58,7 +58,7 @@
</table> </table>
</div> </div>
<div class="col-md map-col"> <div class="col-md map-col">
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map> <app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span> <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span> <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span> <span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
<app-closing-type *ngIf="channel.closing_reason" [type]="channel.closing_reason"></app-closing-type>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -30,6 +30,10 @@
font-size: 20px; font-size: 20px;
} }
.badge {
margin-right: 5px;
}
app-fiat { app-fiat {
display: block; display: block;
font-size: 13px; font-size: 13px;

View File

@ -87,8 +87,6 @@
</ng-template> </ng-template>
<ng-template #skeleton> <ng-template #skeleton>
<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>
<tbody> <tbody>

View File

@ -52,7 +52,7 @@
</table> </table>
</div> </div>
<div class="col-md map-col"> <div class="col-md map-col">
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map> <app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="container-xl" *ngIf="(node$ | async) as node"> <div class="container-xl" *ngIf="(node$ | async) as node">
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5> <h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
<div class="title-container mb-2" *ngIf="!error"> <div class="title-container mb-2" *ngIf="!error">
<h1 class="mb-0">{{ node.alias }}</h1> <h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
<span class="tx-link"> <span class="tx-link">
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]"> <a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
{{ node.public_key | shortenString : publicKeySize }} {{ node.public_key | shortenString : publicKeySize }}
@ -131,7 +131,6 @@
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart> <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div> </div>
<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> <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">

View File

@ -22,6 +22,7 @@ export class NodesChannelsMap implements OnInit {
@Input() channel: any[] = []; @Input() channel: any[] = [];
@Input() fitContainer = false; @Input() fitContainer = false;
@Input() hasLocation = true; @Input() hasLocation = true;
@Input() placeholder = false;
@Output() readyEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter();
channelsObservable: Observable<any>; channelsObservable: Observable<any>;
@ -201,11 +202,26 @@ export class NodesChannelsMap implements OnInit {
prepareChartOptions(nodes, channels) { prepareChartOptions(nodes, channels) {
let title: object; let title: object;
if (channels.length === 0) { if (channels.length === 0 && !this.placeholder) {
this.chartOptions = null; this.chartOptions = null;
return; return;
} }
// empty map fallback
if (channels.length === 0 && this.placeholder) {
title = {
textStyle: {
color: 'white',
fontSize: 18
},
text: $localize`No geolocation data available`,
left: 'center',
top: 'center'
};
this.zoom = 1.5;
this.center = [0, 20];
}
this.chartOptions = { this.chartOptions = {
silent: this.style === 'widget', silent: this.style === 'widget',
title: title ?? undefined, title: title ?? undefined,

View File

@ -1,2 +1,9 @@
<div *ngIf="channelsObservable$ | async" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"> <div *ngIf="channelsObservable$ | async" style="min-height: 455px">
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div> </div>

View File

@ -0,0 +1,9 @@
.loading-spinner {
min-height: 455px;
z-index: 100;
}
.spinner-border {
position: relative;
top: 225px;
}

View File

@ -1,8 +1,8 @@
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts'; import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
import { Observable, tap } from 'rxjs'; import { Observable, share, switchMap, tap } from 'rxjs';
import { lerpColor } from 'src/app/shared/graphs.utils'; import { lerpColor } from 'src/app/shared/graphs.utils';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '../lightning-api.service'; import { LightningApiService } from '../lightning-api.service';
@ -25,7 +25,7 @@ export class NodeChannels implements OnChanges {
}; };
channelsObservable$: Observable<any>; channelsObservable$: Observable<any>;
isLoading: true; isLoading = true;
constructor( constructor(
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
@ -41,9 +41,20 @@ export class NodeChannels implements OnChanges {
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active') this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
.pipe( .pipe(
tap((response) => { switchMap((response) => {
const biggestCapacity = response.body[0].capacity; this.isLoading = true;
this.prepareChartOptions(response.body.map(channel => { if ((response.body?.length ?? 0) <= 0) {
return [];
}
return [response.body];
}),
tap((body: any[]) => {
if (body.length === 0) {
this.isLoading = false;
return;
}
const biggestCapacity = body[0].capacity;
this.prepareChartOptions(body.map(channel => {
return { return {
name: channel.node.alias, name: channel.node.alias,
value: channel.capacity, value: channel.capacity,
@ -54,7 +65,9 @@ export class NodeChannels implements OnChanges {
} }
}; };
})); }));
}) this.isLoading = false;
}),
share(),
); );
} }

View File

@ -153,7 +153,12 @@ export class StateService {
if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') { if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
return; return;
} }
const networkMatches = url.match(/^\/(bisq|testnet|liquidtestnet|liquid|signet)/); // horrible network regex breakdown:
// /^\/ starts with a forward slash...
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
// (?:preview\/)? optional "preview" prefix (non-capturing)
// (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1])
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/);
switch (networkMatches && networkMatches[1]) { switch (networkMatches && networkMatches[1]) {
case 'liquid': case 'liquid':
if (this.network !== 'liquid') { if (this.network !== 'liquid') {

View File

@ -63,6 +63,7 @@ import { StatusViewComponent } from '../components/status-view/status-view.compo
import { FeesBoxComponent } from '../components/fees-box/fees-box.component'; import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
import { DifficultyComponent } from '../components/difficulty/difficulty.component'; import { DifficultyComponent } from '../components/difficulty/difficulty.component';
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
@ -138,6 +139,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StatusViewComponent, StatusViewComponent,
FeesBoxComponent, FeesBoxComponent,
DifficultyComponent, DifficultyComponent,
TxBowtieGraphComponent,
TermsOfServiceComponent, TermsOfServiceComponent,
PrivacyPolicyComponent, PrivacyPolicyComponent,
TrademarkPolicyComponent, TrademarkPolicyComponent,
@ -242,6 +244,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
StatusViewComponent, StatusViewComponent,
FeesBoxComponent, FeesBoxComponent,
DifficultyComponent, DifficultyComponent,
TxBowtieGraphComponent,
TermsOfServiceComponent, TermsOfServiceComponent,
PrivacyPolicyComponent, PrivacyPolicyComponent,
TrademarkPolicyComponent, TrademarkPolicyComponent,

View File

@ -1287,9 +1287,9 @@ case $OS in
osPackageInstall ${CLN_PKG} osPackageInstall ${CLN_PKG}
echo "[*] Installing Core Lightning mainnet Cronjob" echo "[*] Installing Core Lightning mainnet Cronjob"
crontab_cln+='@reboot sleep 30 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n' crontab_cln+='@reboot sleep 60 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n'
crontab_cln+='@reboot sleep 60 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network testnet\n' crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network testnet\n'
crontab_cln+='@reboot sleep 120 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
echo "${crontab_cln}" | crontab -u "${CLN_USER}" - echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
;; ;;
Debian) Debian)

View File

@ -12,7 +12,10 @@ ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2)
ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2) ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
# get mysql credentials # get mysql credentials
. /mempool/mysql_credentials MYSQL_CRED_FILE=${HOME}/mempool/mysql_credentials
if [ -f "${MYSQL_CRED_FILE}" ];then
. ${MYSQL_CRED_FILE}
fi
if [ -f "${LOCKFILE}" ];then if [ -f "${LOCKFILE}" ];then
echo "upgrade already running? check lockfile ${LOCKFILE}" echo "upgrade already running? check lockfile ${LOCKFILE}"
@ -63,6 +66,19 @@ build_frontend()
npm run build || exit 1 npm run build || exit 1
} }
build_unfurler()
{
local site="$1"
echo "[*] Building unfurler for ${site}"
[ -z "${HASH}" ] && exit 1
cd "$HOME/${site}/unfurler" || exit 1
if [ ! -e "config.json" ];then
cp "${HOME}/mempool/production/unfurler-config.${site}.json" "config.json"
fi
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install || exit 1
npm run build || exit 1
}
build_backend() build_backend()
{ {
local site="$1" local site="$1"
@ -128,6 +144,11 @@ for repo in $backend_repos;do
update_repo "${repo}" update_repo "${repo}"
done done
# build unfurlers
for repo in mainnet liquid;do
build_unfurler "${repo}"
done
# build backends # build backends
for repo in $backend_repos;do for repo in $backend_repos;do
build_backend "${repo}" build_backend "${repo}"

View File

@ -1,2 +1,8 @@
#!/usr/bin/env zsh #!/usr/bin/env zsh
killall sh node killall sh
killall node
killall chrome
killall xinit
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
kill $pid
done

View File

@ -2,7 +2,29 @@
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh" source "$NVM_DIR/nvm.sh"
# start all mempool backends that exist
for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do
cd "${HOME}/${site}/backend/" && \ cd "${HOME}/${site}/backend/" && \
echo "starting mempool backend: ${site}" && \
screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done' screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
done done
# only start unfurler if GPU present
if pciconf -lv|grep -i nvidia >/dev/null 2>&1;then
export DISPLAY=:0
screen -dmS x startx
sleep 3
for site in mainnet liquid;do
cd "$HOME/${site}/unfurler" && \
echo "starting mempool unfurler: ${site}" && \
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
done
fi
# start nginx warm cacher
for site in mainnet;do
echo "starting mempool cache warmer: ${site}"
screen -dmS "warmer-${site}" $HOME/mempool/production/nginx-cache-warmer
done
exit 0

View File

@ -2,6 +2,12 @@
hostname=$(hostname) hostname=$(hostname)
slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`) slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`)
warm()
{
echo "$1"
curl -i -s "$1" | head -1
}
while true while true
do for url in / \ do for url in / \
'/api/v1/blocks' \ '/api/v1/blocks' \
@ -81,14 +87,14 @@ do for url in / \
'/api/v1/lightning/channels-geo?style=graph' \ '/api/v1/lightning/channels-geo?style=graph' \
do do
curl -s "https://${hostname}${url}" >/dev/null warm "https://${hostname}${url}"
done done
for slug in $slugs for slug in $slugs
do do
curl -s "https://${hostname}/api/v1/mining/pool/${slug}" >/dev/null warm "https://${hostname}/api/v1/mining/pool/${slug}"
curl -s "https://${hostname}/api/v1/mining/pool/${slug}/hashrate" >/dev/null warm "https://${hostname}/api/v1/mining/pool/${slug}/hashrate"
curl -s "https://${hostname}/api/v1/mining/pool/${slug}/blocks" >/dev/null warm "https://${hostname}/api/v1/mining/pool/${slug}/blocks"
done done
sleep 10 sleep 10

View File

@ -1,62 +0,0 @@
#!/usr/bin/env zsh
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin
HOSTNAME=$(hostname)
LOCATION=$(hostname|cut -d . -f2)
LOCKFILE="${HOME}/lock"
REF=$(echo "${1:=origin/master}"|sed -e 's!:!/!')
if [ -f "${LOCKFILE}" ];then
echo "upgrade already running? check lockfile ${LOCKFILE}"
exit 1
fi
# on exit, remove lockfile but preserve exit code
trap "rv=\$?; rm -f "${LOCKFILE}"; exit \$rv" INT TERM EXIT
# create lockfile
touch "${LOCKFILE}"
# notify logged in users
echo "Upgrading unfurler to ${REF}" | wall
update_repo()
{
echo "[*] Upgrading unfurler to ${REF}"
cd "$HOME/unfurl/unfurler" || exit 1
git fetch origin || exit 1
for remote in origin;do
git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1
git fetch "${remote}" || exit 1
done
if [ $(git tag -l "${REF}") ];then
git reset --hard "tags/${REF}" || exit 1
elif [ $(git branch -r -l "origin/${REF}") ];then
git reset --hard "origin/${REF}" || exit 1
else
git reset --hard "${REF}" || exit 1
fi
export HASH=$(git rev-parse HEAD)
}
build_backend()
{
echo "[*] Building backend for unfurler"
[ -z "${HASH}" ] && exit 1
cd "$HOME/unfurl/unfurler" || exit 1
if [ ! -e "config.json" ];then
cp "${HOME}/unfurl/production/mempool-config.unfurl.json" "config.json"
fi
npm install || exit 1
npm run build || exit 1
}
update_repo
build_backend
# notify everyone
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}"
exit 0

View File

@ -1,2 +0,0 @@
#!/usr/bin/env zsh
killall sh node

View File

@ -1,6 +0,0 @@
#!/usr/bin/env zsh
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
cd "${HOME}/unfurl/unfurler/" && \
screen -dmS "unfurl" sh -c 'while true;do npm run start-production;sleep 1;done'

View File

@ -0,0 +1,17 @@
{
"SERVER": {
"HOST": "https://liquid.network",
"HTTP_PORT": 8002
},
"MEMPOOL": {
"HTTP_HOST": "https://liquid.network",
"HTTP_PORT": 443,
"NETWORK": "liquid"
},
"PUPPETEER": {
"CLUSTER_SIZE": 8,
"EXEC_PATH": "/usr/local/bin/chrome",
"MAX_PAGE_AGE": 86400,
"RENDER_TIMEOUT": 3000
}
}

View File

@ -0,0 +1,17 @@
{
"SERVER": {
"HOST": "https://mempool.space",
"HTTP_PORT": 8001
},
"MEMPOOL": {
"HTTP_HOST": "https://mempool.space",
"HTTP_PORT": 443,
"NETWORK": "bitcoin"
},
"PUPPETEER": {
"CLUSTER_SIZE": 8,
"EXEC_PATH": "/usr/local/bin/chrome",
"MAX_PAGE_AGE": 86400,
"RENDER_TIMEOUT": 3000
}
}

View File

@ -11,7 +11,7 @@
"tsc": "./node_modules/typescript/bin/tsc", "tsc": "./node_modules/typescript/bin/tsc",
"build": "npm run tsc", "build": "npm run tsc",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=4096 dist/index.js", "unfurler": "node --max-old-space-size=4096 dist/index.js",
"lint": "./node_modules/.bin/eslint . --ext .ts", "lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""

View File

@ -41,6 +41,6 @@
"--use-mock-keychain", "--use-mock-keychain",
"--ignore-gpu-blacklist", "--ignore-gpu-blacklist",
"--ignore-gpu-blocklist", "--ignore-gpu-blocklist",
"--use-gl=swiftshader" "--use-gl=egl"
] ]
} }