mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 02:11:49 +01:00
Merge branch 'master' into simon/custom-lazy-loading-strategy
This commit is contained in:
commit
7e1ab55c01
@ -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';
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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">‎{{ (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]="'‎' + (tx.size | bytes: 2)"></span>
|
||||||
<td i18n="block.timestamp">Timestamp</td>
|
<span [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
|
||||||
<td>
|
</p>
|
||||||
‎{{ 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">
|
|
||||||
‎{{ 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]="'‎' + (tx.size | bytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.weight">Weight</td>
|
|
||||||
<td [innerHTML]="'‎' + (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">
|
|
||||||
|
|
||||||
<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]="'‎' + (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]="'‎' + (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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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 |
@ -0,0 +1,15 @@
|
|||||||
|
.bowtie {
|
||||||
|
.line {
|
||||||
|
fill: none;
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
stroke: url(#input-gradient);
|
||||||
|
}
|
||||||
|
&.output {
|
||||||
|
stroke: url(#output-gradient);
|
||||||
|
}
|
||||||
|
&.fee {
|
||||||
|
stroke: url(#fee-gradient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.loading-spinner {
|
||||||
|
min-height: 455px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
position: relative;
|
||||||
|
top: 225px;
|
||||||
|
}
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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') {
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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}"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
killall sh node
|
|
@ -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'
|
|
17
production/unfurler-config.liquid.json
Normal file
17
production/unfurler-config.liquid.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
17
production/unfurler-config.mainnet.json
Normal file
17
production/unfurler-config.mainnet.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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}\""
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user