Merge branch 'master' into simon/search-bar-click-outside

This commit is contained in:
Felipe Knorr Kuhn authored and GitHub committed 2022-11-07 07:21:19 -08:00
commit 779cc2ba04
78 files changed
+78060 -16589

No files matched your search

+1 -1
View File
@@ -113,7 +113,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
* French @Bayernatoor
* Korean @kcalvinalvinn
* Italian @HodlBits
* Hebrew @Sh0ham
* Hebrew @rapidlab309
* Georgian @wyd_idk
* Hungarian @btcdragonlord
* Dutch @m__btc
+3 -3
View File
@@ -3,9 +3,9 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool.ninja");
entry.target = entry.target.replace("liquid.network", "liquid.place");
entry.target = entry.target.replace("bisq.markets", "bisq.ninja");
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
});
module.exports = PROXY_CONFIG;
@@ -1,21 +1,22 @@
<div class="container-xl" (window:resize)="onResize($event)">
<div *ngIf="(auditObservable$ | async) as blockAudit; else skeleton">
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-title">Block </span>
&nbsp;
<a [routerLink]="['/block/' | relativeUrl, blockAudit.id]">{{ blockAudit.height }}</a>
&nbsp;
<span i18n="shared.template-vs-mined">Template vs Mined</span>
</span>
</h1>
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-audit-title">Block Audit</span>
&nbsp;
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
&nbsp;
</span>
</h1>
<div class="grow"></div>
<div class="grow"></div>
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">&#10005;</button>
</div>
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">&#10005;</button>
</div>
<div *ngIf="!error && !isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
@@ -26,8 +27,8 @@
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, blockAudit.id]" title="{{ blockAudit.id }}">{{ blockAudit.id | shortenString : 13 }}</a>
<app-clipboard class="d-none d-sm-inline-block" [text]="blockAudit.id"></app-clipboard>
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
</td>
</tr>
<tr>
@@ -40,6 +41,10 @@
</div>
</td>
</tr>
<tr>
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
<td>{{ blockAudit.tx_count }}</td>
</tr>
<tr>
<td i18n="blockAudit.size">Size</td>
<td [innerHTML]="'&lrm;' + (blockAudit.size | bytes: 2)"></td>
@@ -57,21 +62,25 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
<td>{{ blockAudit.tx_count }}</td>
</tr>
<tr>
<td i18n="block.match-rate">Match rate</td>
<td i18n="block.health">Block health</td>
<td>{{ blockAudit.matchRate }}%</td>
</tr>
<tr>
<td i18n="block.missing-txs">Missing txs</td>
<td i18n="block.missing-txs">Removed txs</td>
<td>{{ blockAudit.missingTxs.length }}</td>
</tr>
<tr>
<td i18n="block.missing-txs">Omitted txs</td>
<td>{{ numMissing }}</td>
</tr>
<tr>
<td i18n="block.added-txs">Added txs</td>
<td>{{ blockAudit.addedTxs.length }}</td>
</tr>
<tr>
<td i18n="block.missing-txs">Included txs</td>
<td>{{ numUnexpected }}</td>
</tr>
</tbody>
</table>
</div>
@@ -79,33 +88,110 @@
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3" *ngIf="isMobile">
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.missing-txs"
fragment="missing" (click)="changeMode('missing')">Missing</a>
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.added-txs"
fragment="added" (click)="changeMode('added')">Added</a>
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</div>
<ng-template [ngIf]="!error && isLoading">
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-audit-title">Block Audit</span>
&nbsp;
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
&nbsp;
</span>
</h1>
<div class="grow"></div>
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">&#10005;</button>
</div>
<!-- OVERVIEW -->
<div class="box mb-3">
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
<!-- RIGHT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
</div> <!-- row -->
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div *ngIf="error && error.status === 404; else generalError" class="text-center">
<br>
<b i18n="error.audit-unavailable">audit unavailable</b>
<br><br>
<i>{{ error.error }}</i>
<br>
<br>
</div>
<ng-template #generalError>
<div class="text-center">
<br>
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error }}</i>
<br>
<br>
</div>
</ng-template>
</ng-template>
<!-- VISUALIZATIONS -->
<div class="box">
<div class="box" *ngIf="!error">
<div class="row">
<!-- MISSING TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled">
<app-block-overview-graph #blockGraphTemplate [isLoading]="isLoading" [resolution]="75"
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
</div>
<!-- ADDED TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
<app-block-overview-graph #blockGraphMined [isLoading]="isLoading" [resolution]="75"
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
</div>
</div> <!-- row -->
</div> <!-- box -->
<ng-template #skeleton></ng-template>
</div>
@@ -37,4 +37,8 @@
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}
@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map, share, switchMap, tap } from 'rxjs/operators';
import { Subscription, combineLatest } from 'rxjs';
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
@@ -22,22 +22,30 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv
}
`],
})
export class BlockAuditComponent implements OnInit, OnDestroy {
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
blockAudit: BlockAudit = undefined;
transactions: string[];
auditObservable$: Observable<BlockAudit>;
auditSubscription: Subscription;
urlFragmentSubscription: Subscription;
paginationMaxSize: number;
page = 1;
itemsPerPage: number;
mode: 'missing' | 'added' = 'missing';
mode: 'projected' | 'actual' = 'projected';
error: any;
isLoading = true;
webGlEnabled = true;
isMobile = window.innerWidth <= 767.98;
@ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent;
@ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent;
childChangeSubscription: Subscription;
blockHash: string;
numMissing: number = 0;
numUnexpected: number = 0;
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
@@ -48,73 +56,137 @@ export class BlockAuditComponent implements OnInit, OnDestroy {
this.webGlEnabled = detectWebGL();
}
ngOnDestroy(): void {
ngOnDestroy() {
this.childChangeSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
}
ngOnInit(): void {
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
this.auditObservable$ = this.route.paramMap.pipe(
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
if (fragment === 'actual') {
this.mode = 'actual';
} else {
this.mode = 'projected'
}
this.setupBlockGraphs();
});
this.auditSubscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const blockHash: string = params.get('id') || '';
return this.apiService.getBlockAudit$(blockHash)
this.blockHash = params.get('id') || null;
if (!this.blockHash) {
return null;
}
return this.apiService.getBlockAudit$(this.blockHash)
.pipe(
map((response) => {
const blockAudit = response.body;
for (let i = 0; i < blockAudit.template.length; ++i) {
if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) {
blockAudit.template[i].status = 'missing';
} else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) {
blockAudit.template[i].status = 'added';
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
this.numMissing = 0;
this.numUnexpected = 0;
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
blockAudit.template[i].status = 'found';
tx.status = 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (let i = 0; i < blockAudit.transactions.length; ++i) {
if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) {
blockAudit.transactions[i].status = 'missing';
} else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) {
blockAudit.transactions[i].status = 'added';
for (const [index, tx] of blockAudit.transactions.entries()) {
if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (index === 0 || inTemplate[tx.txid]) {
tx.status = 'found';
} else {
blockAudit.transactions[i].status = 'found';
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
return blockAudit;
}),
tap((blockAudit) => {
this.changeMode(this.mode);
if (this.blockGraphTemplate) {
this.blockGraphTemplate.destroy();
this.blockGraphTemplate.setup(blockAudit.template);
}
if (this.blockGraphMined) {
this.blockGraphMined.destroy();
this.blockGraphMined.setup(blockAudit.transactions);
}
this.isLoading = false;
}),
})
);
}),
share()
);
catchError((err) => {
console.log(err);
this.error = err;
this.isLoading = false;
return null;
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoading = false;
});
}
ngAfterViewInit() {
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
})
}
setupBlockGraphs() {
if (this.blockAudit) {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit.transactions);
} else {
graph.setup(this.blockAudit.template);
}
})
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit.transactions);
})
}
}
onResize(event: any) {
this.isMobile = event.target.innerWidth <= 767.98;
const isMobile = event.target.innerWidth <= 767.98;
const changed = isMobile !== this.isMobile;
this.isMobile = isMobile;
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
if (changed) {
this.changeMode(this.mode);
}
}
changeMode(mode: 'missing' | 'added') {
changeMode(mode: 'projected' | 'actual') {
this.router.navigate([], { fragment: mode });
this.mode = mode;
}
onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]);
}
pageChange(page: number, target: HTMLElement) {
}
}
@@ -7,6 +7,15 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
const auditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('03E1E5'),
selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
}
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
@@ -25,7 +34,7 @@ export default class TxView implements TransactionStripped {
vsize: number;
value: number;
feerate: number;
status?: 'found' | 'missing' | 'added';
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
initialised: boolean;
vertexArray: FastVertexArray;
@@ -142,16 +151,23 @@ export default class TxView implements TransactionStripped {
}
getColor(): Color {
// Block audit
if (this.status === 'missing') {
return hexToColor('039BE5');
} else if (this.status === 'added') {
return hexToColor('D81B60');
}
// Block component
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Block audit
switch(this.status) {
case 'censored':
return auditColors.censored;
case 'missing':
return auditColors.missing;
case 'added':
return auditColors.added;
case 'selected':
return auditColors.selected;
case 'found':
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
default:
return feeLevelColor;
}
}
}
@@ -163,3 +179,22 @@ function hexToColor(hex: string): Color {
a: 1
};
}
function desaturate(color: Color, amount: number): Color {
const gray = (color.r + color.g + color.b) / 6;
return {
r: color.r + ((gray - color.r) * amount),
g: color.g + ((gray - color.g) * amount),
b: color.b + ((gray - color.b) * amount),
a: color.a,
};
}
function darken(color: Color, amount: number): Color {
return {
r: color.r * amount,
g: color.g * amount,
b: color.b * amount,
a: color.a,
}
}
@@ -32,6 +32,16 @@
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (vsize | vbytes: 2)"></td>
</tr>
<tr *ngIf="tx && tx.status && tx.status.length">
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
<ng-container [ngSwitch]="tx?.status">
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.missing">missing</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
</ng-container>
</tr>
</tbody>
</table>
</div>
@@ -110,6 +110,13 @@
</span>
</td>
</tr>
<tr *ngIf="indexingAvailable">
<td i18n="block.health">Block health</td>
<td>
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
<span *ngIf="block.extras?.matchRate == null" i18n="unknown">Unknown</span>
</td>
</tr>
</ng-template>
</tbody>
</table>
@@ -47,6 +47,7 @@ export class BlockComponent implements OnInit, OnDestroy {
transactionsError: any = null;
overviewError: any = null;
webGlEnabled = true;
indexingAvailable = false;
transactionSubscription: Subscription;
overviewSubscription: Subscription;
@@ -86,6 +87,9 @@ export class BlockComponent implements OnInit, OnDestroy {
this.timeLtr = !!ltr;
});
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
this.stateService.env.MINING_DASHBOARD === true);
this.txsLoadingStatus$ = this.route.paramMap
.pipe(
switchMap(() => this.stateService.loadingIndicators$),
@@ -14,6 +14,8 @@
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
<th *ngIf="indexingAvailable" class="health text-left" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
@@ -37,12 +39,30 @@
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
</div>
</td>
<td class="timestamp" *ngIf="!widget">
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td>
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
</td>
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
<div class="progress progress-health">
<div class="progress-bar progress-bar-health" role="progressbar"
[ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
<div class="progress-text">
<span>{{ block.extras.matchRate }}%</span>
</div>
</div>
</a>
<div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
<div class="progress-bar progress-bar-health" role="progressbar"
[ngStyle]="{'width': '100%' }"></div>
<div class="progress-text">
<span>~</span>
</div>
</div>
</td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
</td>
@@ -77,6 +97,9 @@
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span>
</td>
<td *ngIf="indexingAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
@@ -63,7 +63,7 @@ tr, td, th {
}
.height {
width: 10%;
width: 8%;
}
.height.widget {
width: 15%;
@@ -77,12 +77,18 @@ tr, td, th {
.timestamp {
width: 18%;
@media (max-width: 900px) {
@media (max-width: 1100px) {
display: none;
}
}
.timestamp.legacy {
width: 20%;
@media (max-width: 1100px) {
display: table-cell;
}
@media (max-width: 850px) {
display: none;
}
}
.mined {
@@ -93,6 +99,10 @@ tr, td, th {
}
.mined.legacy {
width: 15%;
@media (max-width: 1000px) {
padding-right: 20px;
width: 20%;
}
@media (max-width: 576px) {
display: table-cell;
}
@@ -100,6 +110,7 @@ tr, td, th {
.txs {
padding-right: 40px;
width: 8%;
@media (max-width: 1100px) {
padding-right: 10px;
}
@@ -113,17 +124,21 @@ tr, td, th {
}
.txs.widget {
padding-right: 0;
display: none;
@media (max-width: 650px) {
display: none;
}
}
.txs.legacy {
padding-right: 80px;
width: 10%;
width: 18%;
display: table-cell;
@media (max-width: 1000px) {
padding-right: 20px;
}
}
.fees {
width: 10%;
width: 8%;
@media (max-width: 650px) {
display: none;
}
@@ -133,7 +148,7 @@ tr, td, th {
}
.reward {
width: 10%;
width: 8%;
@media (max-width: 576px) {
width: 7%;
padding-right: 30px;
@@ -152,8 +167,11 @@ tr, td, th {
}
.size {
width: 12%;
width: 10%;
@media (max-width: 1000px) {
width: 13%;
}
@media (max-width: 950px) {
width: 15%;
}
@media (max-width: 650px) {
@@ -164,12 +182,34 @@ tr, td, th {
}
}
.size.legacy {
width: 20%;
width: 30%;
@media (max-width: 576px) {
display: table-cell;
}
}
.health {
width: 10%;
@media (max-width: 1000px) {
width: 13%;
}
@media (max-width: 950px) {
display: none;
}
}
.health.widget {
width: 25%;
@media (max-width: 1000px) {
display: none;
}
@media (max-width: 767px) {
display: table-cell;
}
@media (max-width: 500px) {
display: none;
}
}
/* Tooltip text */
.tooltip-custom {
position: relative;
@@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
import { StateService } from '../../services/state.service';
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
}
}
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
@@ -43,7 +43,7 @@ export class SearchFormComponent implements OnInit {
@Output() searchTriggered = new EventEmitter();
@ViewChild('searchResults') searchResults: SearchResultsComponent;
@HostListener('keydown', ['$event']) keydown($event) {
@HostListener('keydown', ['$event']) keydown($event): void {
this.handleKeyDown($event);
}
@@ -58,7 +58,7 @@ export class SearchFormComponent implements OnInit {
private elementRef: ElementRef,
) { }
ngOnInit() {
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.searchForm = this.formBuilder.group({
@@ -72,70 +72,111 @@ export class SearchFormComponent implements OnInit {
});
}
this.typeAhead$ = this.searchForm.get('searchText').valueChanges
.pipe(
map((text) => {
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
return text.substr(1);
}
return text.trim();
}),
debounceTime(200),
distinctUntilChanged(),
switchMap((text) => {
if (!text.length) {
return of([
'',
[],
{
nodes: [],
channels: [],
}
]);
}
this.isTypeaheading$.next(true);
if (!this.stateService.env.LIGHTNING) {
return zip(
of(text),
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
[{ nodes: [], channels: [] }],
of(this.regexBlockheight.test(text)),
);
}
const searchText$ = this.searchForm.get('searchText').valueChanges
.pipe(
map((text) => {
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
return text.substr(1);
}
return text.trim();
}),
distinctUntilChanged(),
);
const searchResults$ = searchText$.pipe(
debounceTime(200),
switchMap((text) => {
if (!text.length) {
return of([
[],
{ nodes: [], channels: [] }
]);
}
this.isTypeaheading$.next(true);
if (!this.stateService.env.LIGHTNING) {
return zip(
of(text),
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
[{ nodes: [], channels: [] }],
);
}
return zip(
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
nodes: [],
channels: [],
}))),
);
}),
tap((result: any[]) => {
this.isTypeaheading$.next(false);
})
);
this.typeAhead$ = combineLatest(
[
searchText$,
searchResults$.pipe(
startWith([
[],
{
nodes: [],
channels: [],
}
]))
]
).pipe(
map((latestData) => {
const searchText = latestData[0];
if (!searchText.length) {
return {
searchText: '',
hashQuickMatch: false,
blockHeight: false,
txId: false,
address: false,
addresses: [],
nodes: [],
channels: [],
}))),
);
}),
map((result: any[]) => {
this.isTypeaheading$.next(false);
if (this.network === 'bisq') {
return result[0].map((address: string) => 'B' + address);
};
}
const result = latestData[1];
const addressPrefixSearchResults = result[0];
const lightningResults = result[1];
if (this.network === 'bisq') {
return searchText.map((address: string) => 'B' + address);
}
const matchesBlockHeight = this.regexBlockheight.test(searchText);
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText);
const matchesAddress = this.regexAddress.test(searchText);
return {
searchText: result[0],
blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [],
addresses: result[1],
nodes: result[2].nodes,
channels: result[2].channels,
totalResults: result[1].length + result[2].nodes.length + result[2].channels.length,
searchText: searchText,
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),
blockHeight: matchesBlockHeight,
txId: matchesTxId,
blockHash: matchesBlockHash,
address: matchesAddress,
addresses: addressPrefixSearchResults,
nodes: lightningResults.nodes,
channels: lightningResults.channels,
};
})
);
}
handleKeyDown($event) {
handleKeyDown($event): void {
this.searchResults.handleKeyDown($event);
}
itemSelected() {
itemSelected(): void {
setTimeout(() => this.search());
}
selectedResult(result: any) {
selectedResult(result: any): void {
if (typeof result === 'string') {
this.search(result);
} else if (typeof result === 'number') {
@@ -147,7 +188,7 @@ export class SearchFormComponent implements OnInit {
}
}
search(result?: string) {
search(result?: string): void {
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
@@ -181,7 +222,7 @@ export class SearchFormComponent implements OnInit {
}
}
navigate(url: string, searchText: string, extras?: any) {
navigate(url: string, searchText: string, extras?: any): void {
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
@@ -1,14 +1,32 @@
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.blockHeight.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
<ng-template [ngIf]="results.blockHeight.length">
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
<ng-template [ngIf]="results.blockHeight">
<div class="card-title">Bitcoin Block Height</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
Go to "{{ results.searchText }}"
</button>
</ng-template>
<ng-template [ngIf]="results.txId">
<div class="card-title">Bitcoin Transaction</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
Go to "{{ results.searchText | shortenString : 13 }}"
</button>
</ng-template>
<ng-template [ngIf]="results.address">
<div class="card-title">Bitcoin Address</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
Go to "{{ results.searchText | shortenString : isMobile ? 20 : 30 }}"
</button>
</ng-template>
<ng-template [ngIf]="results.blockHash">
<div class="card-title">Bitcoin Block</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
Go to "{{ results.searchText | shortenString : 13 }}"
</button>
</ng-template>
<ng-template [ngIf]="results.addresses.length">
<div class="card-title" *ngIf="stateService.env.LIGHTNING">Bitcoin Addresses</div>
<div class="card-title">Bitcoin Addresses</div>
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
<button (click)="clickItem(results.blockHeight.length + i)" [class.active]="(results.blockHeight.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
</button>
</ng-template>
@@ -16,7 +34,7 @@
<ng-template [ngIf]="results.nodes.length">
<div class="card-title">Lightning Nodes</div>
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
<button (click)="clickItem(results.blockHeight.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.blockHeight.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
</button>
</ng-template>
@@ -24,7 +42,7 @@
<ng-template [ngIf]="results.channels.length">
<div class="card-title">Lightning Channels</div>
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
<button (click)="clickItem(results.blockHeight.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.blockHeight.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ channel.id }}</span>
</button>
</ng-template>
@@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
ngOnChanges() {
this.activeIdx = 0;
if (this.results) {
this.resultsFlattened = [...this.results.blockHeight, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
}
}
@@ -11,11 +11,15 @@
[showZoom]="false"
></app-mempool-graph>
</div>
<div class="blockchain-wrapper">
<div class="blockchain-wrapper" [dir]="timeLtr ? 'rtl' : 'ltr'" [class.time-ltr]="timeLtr">
<div class="position-container">
<app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
<div id="divider"></div>
<span>
<div class="blocks-wrapper">
<app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
</div>
<div id="divider"></div>
</span>
</div>
</div>
</div>
@@ -31,8 +31,9 @@
.position-container {
position: absolute;
left: 50%;
left: 0;
bottom: 170px;
transform: translateX(50vw);
}
#divider {
@@ -47,9 +48,33 @@
top: -28px;
}
}
&.time-ltr {
.blocks-wrapper {
transform: scaleX(-1);
}
}
}
:host-context(.ltr-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: ltr;
}
}
:host-context(.rtl-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: rtl;
}
}
.tv-container {
display: flex;
margin-top: 0px;
flex-direction: column;
}
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
@@ -6,7 +6,7 @@ import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ActivatedRoute } from '@angular/router';
import { map, scan, startWith, switchMap, tap } from 'rxjs/operators';
import { interval, merge, Observable } from 'rxjs';
import { interval, merge, Observable, Subscription } from 'rxjs';
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
@@ -15,11 +15,13 @@ import { ChangeDetectionStrategy } from '@angular/core';
styleUrls: ['./television.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TelevisionComponent implements OnInit {
export class TelevisionComponent implements OnInit, OnDestroy {
mempoolStats: OptimizedMempoolStats[] = [];
statsSubscription$: Observable<OptimizedMempoolStats[]>;
fragment: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
constructor(
private websocketService: WebsocketService,
@@ -37,6 +39,10 @@ export class TelevisionComponent implements OnInit {
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.statsSubscription$ = merge(
this.stateService.live2Chart$.pipe(map(stats => [stats])),
this.route.fragment
@@ -70,4 +76,8 @@ export class TelevisionComponent implements OnInit {
})
);
}
ngOnDestroy() {
this.timeLtrSubscription.unsubscribe();
}
}
@@ -190,7 +190,7 @@
<br>
<ng-container *ngIf="showFlow; else flowPlaceholder">
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
</div>
@@ -210,8 +210,6 @@
[network]="network"
[tooltip]="true"
[inputIndex]="inputIndex" [outputIndex]="outputIndex"
(selectInput)="selectInput($event)"
(selectOutput)="selectOutput($event)"
>
</tx-bowtie-graph>
</div>
@@ -238,7 +236,7 @@
</div>
<div class="title-buttons">
<button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
</div>
@@ -329,7 +327,7 @@
<br>
<ng-container *ngIf="showFlow">
<ng-container *ngIf="flowEnabled">
<div class="title">
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
</div>
@@ -18,6 +18,7 @@ import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-transaction',
@@ -40,6 +41,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txReplacedSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
@@ -49,12 +52,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
liquidUnblinding = new LiquidUnblinding();
inputIndex: number;
outputIndex: number;
showFlow: boolean = true;
graphExpanded: boolean = false;
graphWidth: number = 1000;
graphHeight: number = 360;
inOutLimit: number = 150;
maxInOut: number = 0;
flowPrefSubscription: Subscription;
hideFlow: boolean = this.stateService.hideFlow.value;
overrideFlowPreference: boolean = null;
flowEnabled: boolean;
tooltipPosition: { x: number, y: number };
@@ -64,6 +70,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
constructor(
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
private websocketService: WebsocketService,
@@ -78,12 +85,26 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
(network) => (this.network = network)
);
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
this.hideFlow = !!hide;
this.setFlowEnabled();
});
this.timeAvg$ = timer(0, 1000)
.pipe(
switchMap(() => this.stateService.difficultyAdjustment$),
map((da) => da.timeAvg)
);
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.fragmentParams = new URLSearchParams(fragment || '');
const vin = parseInt(this.fragmentParams.get('vin'), 10);
const vout = parseInt(this.fragmentParams.get('vout'), 10);
this.inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
});
this.fetchCpfpSubscription = this.fetchCpfp$
.pipe(
switchMap((txId) =>
@@ -123,13 +144,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
switchMap((params: ParamMap) => {
const urlMatch = (params.get('id') || '').split(':');
if (urlMatch.length === 2 && urlMatch[1].length === 64) {
this.inputIndex = parseInt(urlMatch[0], 10);
this.outputIndex = null;
const vin = parseInt(urlMatch[0], 10);
this.txId = urlMatch[1];
// rewrite legacy vin syntax
if (!isNaN(vin)) {
this.fragmentParams.set('vin', vin.toString());
this.fragmentParams.delete('vout');
}
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',
fragment: this.fragmentParams.toString(),
});
} else {
this.txId = urlMatch[0];
this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
this.inputIndex = null;
const vout = parseInt(urlMatch[1], 10);
if (urlMatch.length > 1 && !isNaN(vout)) {
// rewrite legacy vout syntax
this.fragmentParams.set('vout', vout.toString());
this.fragmentParams.delete('vin');
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',
fragment: this.fragmentParams.toString(),
});
}
}
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
@@ -213,6 +250,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCpfp$.next(this.tx.txid);
}
}
setTimeout(() => { this.applyFragment(); }, 0);
},
(error) => {
this.error = error;
@@ -245,11 +283,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
if (params.showFlow === 'false') {
this.showFlow = false;
this.overrideFlowPreference = false;
} else if (params.showFlow === 'true') {
this.overrideFlowPreference = true;
} else {
this.showFlow = true;
this.setGraphSize();
this.overrideFlowPreference = null;
}
this.setFlowEnabled();
this.setGraphSize();
});
}
@@ -325,15 +366,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
toggleGraph() {
this.showFlow = !this.showFlow;
const showFlow = !this.flowEnabled;
this.stateService.hideFlow.next(!showFlow);
this.router.navigate([], {
relativeTo: this.route,
queryParams: { showFlow: this.showFlow },
queryParams: { showFlow: showFlow },
queryParamsHandling: 'merge',
fragment: 'flow'
});
}
setFlowEnabled() {
this.flowEnabled = (this.overrideFlowPreference != null ? this.overrideFlowPreference : !this.hideFlow);
}
expandGraph() {
this.graphExpanded = true;
}
@@ -342,14 +388,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.graphExpanded = false;
}
selectInput(input) {
this.inputIndex = input;
this.outputIndex = null;
}
selectOutput(output) {
this.outputIndex = output;
this.inputIndex = null;
// simulate normal anchor fragment behavior
applyFragment(): void {
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
if (anchor) {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
}
}
@HostListener('window:resize', ['$event'])
@@ -365,6 +412,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.leaveTransaction();
}
}
@@ -43,7 +43,7 @@
</ng-template>
</ng-template>
<ng-template #defaultPrevout>
<a [routerLink]="['/tx/' | relativeUrl, vin.txid + ':' + vin.vout]" class="red">
<a [routerLink]="['/tx/' | relativeUrl, vin.txid]" [fragment]="'vout=' + vin.vout" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
</ng-template>
@@ -220,7 +220,7 @@
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #spent>
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].vin + ':' + tx._outspends[vindex].txid]" class="red">
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" [fragment]="'vin=' + tx._outspends[vindex].vin" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
<ng-template #outputNoTxId>
@@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core';
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
@@ -43,9 +43,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() inputIndex: number;
@Input() outputIndex: number;
@Output() selectInput = new EventEmitter<number>();
@Output() selectOutput = new EventEmitter<number>();
inputData: Xput[];
outputData: Xput[];
inputs: SvgLine[];
@@ -368,24 +365,42 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
onClick(event, side, index): void {
if (side === 'input') {
const input = this.tx.vin[index];
if (input && input.txid && input.vout != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid + ':' + input.vout], {
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
queryParamsHandling: 'merge',
fragment: 'flow'
fragment: (new URLSearchParams({
flow: '',
vout: input.vout.toString(),
})).toString(),
});
} else if (index != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({
flow: '',
vin: index.toString(),
})).toString(),
});
} else {
this.selectInput.emit(index);
}
} else {
const output = this.tx.vout[index];
const outspend = this.outspends[index];
if (output && outspend && outspend.spent && outspend.txid) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.vin + ':' + outspend.txid], {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
queryParamsHandling: 'merge',
fragment: 'flow'
fragment: (new URLSearchParams({
flow: '',
vin: outspend.vin.toString(),
})).toString(),
});
} else if (index != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
queryParamsHandling: 'merge',
fragment: (new URLSearchParams({
flow: '',
vout: index.toString(),
})).toString(),
});
} else {
this.selectOutput.emit(index);
}
}
}
@@ -13,7 +13,10 @@
<ng-template #notFullyTaproot>
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about fees that saved and could be saved with taproot" ngbTooltip="This transaction uses Taproot and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
<ng-template #noTaproot>
<span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
<span *ngIf="segwitGains.potentialTaprootGains && segwitGains.potentialTaprootGains > 0; else negativeTaprootGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
<ng-template #negativeTaprootGains>
<span *ngIf="!isTaproot; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about using taproot" ngbTooltip="This transaction does not use Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
</ng-template>
<ng-template #taprootButNoGains>
<span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about taproot" ngbTooltip="This transaction uses Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
</ng-template>
@@ -106,6 +106,20 @@
</div>
</div>
<div id="electrs" *ngIf="whichTab === 'electrs'">
<div class="doc-content no-sidebar">
<div class="doc-item-container">
<p class='subtitle'>Hostname</p>
<p>{{plainHostname}}</p>
<p class="subtitle">Port</p>
<p>{{electrsPort}}</p>
<p class="subtitle">SSL</p>
<p>Enabled</p>
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='/enterprise'>sponsors</a> only—whitelisting is required.</p>
</div>
</div>
</div>
</div>
</ng-container>
@@ -1,7 +1,21 @@
.center {
text-align: center;
}
.note {
font-style: italic;
}
.text-small {
font-size: 12px;
}
.container-xl {
display: flex;
min-height: 75vh;
flex-direction: column;
}
code {
background-color: #1d1f31;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
@@ -116,6 +130,10 @@ li.nav-item {
float: right;
}
.doc-content.no-sidebar {
width: 100%
}
h3 {
margin: 2rem 0 0 0;
}
@@ -12,6 +12,8 @@ import { FaqTemplateDirective } from '../faq-template/faq-template.component';
styleUrls: ['./api-docs.component.scss']
})
export class ApiDocsComponent implements OnInit, AfterViewInit {
plainHostname = document.location.hostname;
electrsPort = 0;
hostname = document.location.hostname;
network$: Observable<string>;
active = 0;
@@ -82,6 +84,20 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
this.network$.subscribe((network) => {
this.active = (network === 'liquid' || network === 'liquidtestnet') ? 2 : 0;
switch( network ) {
case "":
this.electrsPort = 50002; break;
case "mainnet":
this.electrsPort = 50002; break;
case "testnet":
this.electrsPort = 60002; break;
case "signet":
this.electrsPort = 60602; break;
case "liquid":
this.electrsPort = 51002; break;
case "liquidtestnet":
this.electrsPort = 51302; break;
}
});
}
@@ -32,6 +32,15 @@
</ng-template>
</li>
<li [ngbNavItem]="3" *ngIf="showElectrsTab" role="presentation">
<a ngbNavLink [routerLink]="['/docs/api/electrs' | relativeUrl]" role="tab">API - Electrum RPC</a>
<ng-template ngbNavContent>
<app-api-docs [whichTab]="'electrs'"></app-api-docs>
</ng-template>
</li>
</ul>
<div id="main-tab-content" [ngbNavOutlet]="nav"></div>
+6 -1
View File
@@ -15,6 +15,7 @@ export class DocsComponent implements OnInit {
env: Env;
showWebSocketTab = true;
showFaqTab = true;
showElectrsTab = true;
@HostBinding('attr.dir') dir = 'ltr';
@@ -34,14 +35,18 @@ export class DocsComponent implements OnInit {
} else if( url[1].path === "rest" ) {
this.activeTab = 1;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else {
} else if( url[1].path === "websocket" ) {
this.activeTab = 2;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else {
this.activeTab = 3;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
}
this.env = this.stateService.env;
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
this.showElectrsTab = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && ( this.stateService.network !== "bisq" );
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
}
@@ -141,7 +141,7 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
status?: 'found' | 'missing' | 'added';
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
}
export interface RewardStats {
@@ -70,7 +70,7 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
status?: 'found' | 'missing' | 'added';
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
}
export interface IBackendInfo {
+13 -1
View File
@@ -110,6 +110,7 @@ export class StateService {
blockScrolling$: Subject<boolean> = new Subject<boolean>();
timeLtr: BehaviorSubject<boolean>;
hideFlow: BehaviorSubject<boolean>;
constructor(
@Inject(PLATFORM_ID) private platformId: any,
@@ -159,6 +160,16 @@ export class StateService {
this.timeLtr.subscribe((ltr) => {
this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false');
});
const savedFlowPreference = this.storageService.getValue('flow-preference');
this.hideFlow = new BehaviorSubject<boolean>(savedFlowPreference === 'hide');
this.hideFlow.subscribe((hide) => {
if (hide) {
this.storageService.setValue('flow-preference', hide ? 'hide' : 'show');
} else {
this.storageService.removeItem('flow-preference');
}
});
}
setNetworkBasedonUrl(url: string) {
@@ -170,7 +181,8 @@ export class StateService {
// (?:[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)/);
// ($|\/) network string must end or end with a slash
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)($|\/)/);
switch (networkMatches && networkMatches[1]) {
case 'liquid':
if (this.network !== 'liquid') {
@@ -46,4 +46,12 @@ export class StorageService {
console.log(e);
}
}
removeItem(key: string): void {
try {
localStorage.removeItem(key);
} catch (e) {
console.log(e);
}
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+9
View File
@@ -668,6 +668,15 @@ h1, h2, h3 {
background-color: #2e324e;
}
.progress.progress-health {
background: repeating-linear-gradient(to right, #2d3348, #2d3348 0%, #105fb0 0%, #1a9436 100%);
justify-content: flex-end;
}
.progress-bar.progress-bar-health {
background: #2d3348;
}
.mt-2-5, .my-2-5 {
margin-top: 0.75rem !important;
}
+2 -1
View File
@@ -16,7 +16,8 @@
],
"lib": [
"es2018",
"dom"
"dom",
"dom.iterable"
]
},
"angularCompilerOptions": {