Merge branch 'master' into nymkappa/clip-label-overflow
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<span i18n="shared.address">Address</span>
|
||||
</app-preview-title>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="col-md table-col">
|
||||
<div class="row d-flex justify-content-between">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.table-col {
|
||||
max-width: calc(100% - 470px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 32px;
|
||||
margin-top: 48px;
|
||||
|
||||
@@ -207,7 +207,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.retryLoadMore = false;
|
||||
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.lastTransactionTxId)
|
||||
this.electrsApiService.getAddressTransactions$(this.address.address, this.lastTransactionTxId)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.length;
|
||||
@@ -217,6 +217,10 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
(error) => {
|
||||
this.isLoadingTransactions = false;
|
||||
this.retryLoadMore = true;
|
||||
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
|
||||
if (error.status === 422) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" name="bisq" width="20" height="20" viewBox="0 0 80 80"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -147,3 +147,18 @@ nav {
|
||||
.navbar-brand {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
<div class="block-overview-graph">
|
||||
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||
<div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
|
||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||
</div>
|
||||
|
||||
<app-block-overview-tooltip
|
||||
[tx]="selectedTx || hoverTx"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
[clickable]="!!selectedTx"
|
||||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
|
||||
<div class="block-overview-graph">
|
||||
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||
<div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
|
||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||
</div>
|
||||
<app-block-overview-tooltip
|
||||
[tx]="selectedTx || hoverTx"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
[clickable]="!!selectedTx"
|
||||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,16 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.grid-align {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 75px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.block-overview-canvas {
|
||||
position: absolute;
|
||||
|
||||
@@ -6,6 +6,8 @@ import TxSprite from './tx-sprite';
|
||||
import TxView from './tx-view';
|
||||
import { Position } from './sprite-types';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-graph',
|
||||
@@ -23,7 +25,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() unavailable: boolean = false;
|
||||
@Input() auditHighlighting: boolean = false;
|
||||
@Input() blockConversion: Price;
|
||||
@Input() pixelAlign: boolean = false;
|
||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||
@Output() txHoverEvent = new EventEmitter<string>();
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
@@ -44,16 +45,25 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
scene: BlockScene;
|
||||
hoverTx: TxView | void;
|
||||
selectedTx: TxView | void;
|
||||
highlightTx: TxView | void;
|
||||
mirrorTx: TxView | void;
|
||||
tooltipPosition: Position;
|
||||
|
||||
readyNextFrame = false;
|
||||
|
||||
searchText: string;
|
||||
searchSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
readonly ngZone: NgZone,
|
||||
readonly elRef: ElementRef,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
|
||||
this.searchText = text;
|
||||
this.updateSearchHighlight();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
@@ -109,6 +119,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.scene.setup(transactions);
|
||||
this.readyNextFrame = true;
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +127,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (this.scene) {
|
||||
this.scene.enter(transactions, direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +135,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (this.scene) {
|
||||
this.scene.exit(direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +143,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (this.scene) {
|
||||
this.scene.replace(transactions || [], direction, sort);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +151,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (this.scene) {
|
||||
this.scene.update(add, remove, change, direction, resetLayout);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +218,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
|
||||
highlighting: this.auditHighlighting });
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
@@ -406,6 +421,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
updateSearchHighlight(): void {
|
||||
if (this.highlightTx && this.highlightTx.txid !== this.searchText && this.scene) {
|
||||
this.scene.setHighlight(this.highlightTx, false);
|
||||
this.start();
|
||||
} else if (this.scene?.txs && this.searchText && this.searchText.length === 64) {
|
||||
this.highlightTx = this.scene.txs[this.searchText];
|
||||
if (this.highlightTx) {
|
||||
this.scene.setHighlight(this.highlightTx, true);
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHighlightingEnabled(enabled: boolean): void {
|
||||
if (this.scene) {
|
||||
this.scene.setHighlighting(enabled);
|
||||
|
||||
@@ -15,7 +15,6 @@ export default class BlockScene {
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
gridSize: number;
|
||||
pixelAlign: boolean;
|
||||
vbytesPerUnit: number;
|
||||
unitPadding: number;
|
||||
unitWidth: number;
|
||||
@@ -24,24 +23,19 @@ export default class BlockScene {
|
||||
animateUntil = 0;
|
||||
dirty: boolean;
|
||||
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
) {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign });
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
if (this.pixelAlign) {
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding);
|
||||
} else {
|
||||
this.unitPadding = width / 500;
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
}
|
||||
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
|
||||
this.unitWidth = this.gridSize - (this.unitPadding * 2);
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
@@ -215,15 +209,18 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }:
|
||||
setHighlight(tx: TxView, value: boolean): void {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||
}
|
||||
|
||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean }
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||
): void {
|
||||
this.orientation = orientation;
|
||||
this.flip = flip;
|
||||
this.vertexArray = vertexArray;
|
||||
this.highlightingEnabled = highlighting;
|
||||
this.pixelAlign = pixelAlign;
|
||||
|
||||
this.scene = {
|
||||
count: 0,
|
||||
@@ -349,12 +346,7 @@ export default class BlockScene {
|
||||
private gridToScreen(position: Square | void): Square {
|
||||
if (position) {
|
||||
const slotSize = (position.s * this.gridSize);
|
||||
let squareSize;
|
||||
if (this.pixelAlign) {
|
||||
squareSize = slotSize - (this.unitPadding);
|
||||
} else {
|
||||
squareSize = slotSize - (this.unitPadding * 2);
|
||||
}
|
||||
const squareSize = slotSize - (this.unitPadding * 2);
|
||||
|
||||
// The grid is laid out notionally left-to-right, bottom-to-top,
|
||||
// so we rotate and/or flip the y axis to match the target configuration.
|
||||
@@ -430,7 +422,7 @@ export default class BlockScene {
|
||||
|
||||
// calculates and returns the size of the tx in multiples of the grid size
|
||||
private txSize(tx: TxView): number {
|
||||
const scale = Math.max(1, Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)));
|
||||
const scale = Math.max(1, Math.round(Math.sqrt(1.1 * tx.vsize / this.vbytesPerUnit)));
|
||||
return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import BlockScene from './block-scene';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
const defaultHighlightColor = hexToColor('800080');
|
||||
|
||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
@@ -37,15 +38,17 @@ export default class TxView implements TransactionStripped {
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
initialised: boolean;
|
||||
vertexArray: FastVertexArray;
|
||||
hover: boolean;
|
||||
highlight: boolean;
|
||||
sprite: TxSprite;
|
||||
hoverColor: Color | void;
|
||||
highlightColor: Color | void;
|
||||
|
||||
screenPosition: Square;
|
||||
gridPosition: Square | void;
|
||||
@@ -150,8 +153,40 @@ export default class TxView implements TransactionStripped {
|
||||
} else {
|
||||
this.hover = false;
|
||||
this.hoverColor = null;
|
||||
if (this.sprite) {
|
||||
this.sprite.resume(hoverTransitionTime);
|
||||
if (this.highlight) {
|
||||
this.setHighlight(true, this.highlightColor);
|
||||
} else {
|
||||
if (this.sprite) {
|
||||
this.sprite.resume(hoverTransitionTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dirty = false;
|
||||
return performance.now() + hoverTransitionTime;
|
||||
}
|
||||
|
||||
// Temporarily override the tx color
|
||||
// returns minimum transition end time
|
||||
setHighlight(highlightOn: boolean, color: Color | void = defaultHighlightColor): number {
|
||||
if (highlightOn) {
|
||||
this.highlight = true;
|
||||
this.highlightColor = color;
|
||||
|
||||
this.sprite.update({
|
||||
...this.highlightColor,
|
||||
duration: hoverTransitionTime,
|
||||
adjust: false,
|
||||
temp: true
|
||||
});
|
||||
} else {
|
||||
this.highlight = false;
|
||||
this.highlightColor = null;
|
||||
if (this.hover) {
|
||||
this.setHover(true, this.hoverColor);
|
||||
} else {
|
||||
if (this.sprite) {
|
||||
this.sprite.resume(hoverTransitionTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dirty = false;
|
||||
@@ -175,6 +210,7 @@ export default class TxView implements TransactionStripped {
|
||||
case 'fullrbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'sigop'"><span class="badge badge-warning" i18n="transaction.audit.sigop">High sigop count</span></td>
|
||||
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<span i18n="shared.block-title">Block</span>
|
||||
</app-preview-title>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="col-sm table-col">
|
||||
<div class="row">
|
||||
<div class="block-titles">
|
||||
<h1 class="title">
|
||||
@@ -71,7 +71,7 @@
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="false"
|
||||
[resolution]="75"
|
||||
[resolution]="80"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
|
||||
@@ -44,11 +44,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-col {
|
||||
max-width: calc(100% - 470px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 470px;
|
||||
min-width: 470px;
|
||||
width: 480px;
|
||||
min-width: 480px;
|
||||
padding: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="col-sm" [class.graph-col]="webGlEnabled && !showAudit">
|
||||
<table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="restOfTable"></ng-container>
|
||||
@@ -110,7 +110,7 @@
|
||||
<app-block-overview-graph
|
||||
#blockGraphActual
|
||||
[isLoading]="isLoadingOverview"
|
||||
[resolution]="75"
|
||||
[resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
@@ -227,7 +227,7 @@
|
||||
<div class="col-sm">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
@@ -239,7 +239,7 @@
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
|
||||
@@ -239,6 +239,7 @@ h1 {
|
||||
.nav-tabs {
|
||||
border-color: white;
|
||||
border-width: 1px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
@@ -293,3 +294,7 @@ h1 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-col {
|
||||
flex-grow: 1.11;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -72,6 +73,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
auditSubscription: Subscription;
|
||||
keyNavigationSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
cacheBlocksSubscription: Subscription;
|
||||
networkChangedSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
nextBlockSubscription: Subscription = undefined;
|
||||
@@ -99,6 +101,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private apiService: ApiService,
|
||||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
@@ -128,19 +131,27 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
|
||||
);
|
||||
|
||||
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||
this.loadedCacheBlock(block);
|
||||
});
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
this.latestBlock = block;
|
||||
this.latestBlocks.unshift(block);
|
||||
this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||
.subscribe((blocks) => {
|
||||
this.latestBlock = blocks[0];
|
||||
this.latestBlocks = blocks;
|
||||
this.setNextAndPreviousBlockLink();
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
for (const block of blocks) {
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
} else if (block.height === this.block?.height) {
|
||||
this.block.stale = true;
|
||||
this.block.canonical = block.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -254,6 +265,13 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.transactionsError = null;
|
||||
this.isLoadingOverview = true;
|
||||
this.overviewError = null;
|
||||
|
||||
const cachedBlock = this.cacheService.getCachedBlock(block.height);
|
||||
if (!cachedBlock) {
|
||||
this.cacheService.loadBlock(block.height);
|
||||
} else {
|
||||
this.loadedCacheBlock(cachedBlock);
|
||||
}
|
||||
}),
|
||||
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
|
||||
shareReplay(1)
|
||||
@@ -352,7 +370,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
if (isFresh[tx.txid]) {
|
||||
tx.status = 'fresh';
|
||||
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
|
||||
tx.status = 'freshcpfp';
|
||||
} else {
|
||||
tx.status = 'fresh';
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
@@ -459,6 +481,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.auditSubscription?.unsubscribe();
|
||||
this.keyNavigationSubscription?.unsubscribe();
|
||||
this.blocksSubscription?.unsubscribe();
|
||||
this.cacheBlocksSubscription?.unsubscribe();
|
||||
this.networkChangedSubscription?.unsubscribe();
|
||||
this.queryParamsSubscription?.unsubscribe();
|
||||
this.timeLtrSubscription?.unsubscribe();
|
||||
@@ -679,4 +702,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
loadedCacheBlock(block: BlockExtended): void {
|
||||
if (this.block && block.height === this.block.height && block.id !== this.block.id) {
|
||||
this.block.stale = true;
|
||||
this.block.canonical = block.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||
markHeight: number;
|
||||
chainTip: number;
|
||||
pendingMarkBlock: { animate: boolean, newBlockFromLeft: boolean };
|
||||
blocksSubscription: Subscription;
|
||||
blockPageSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
tabHiddenSubscription: Subscription;
|
||||
markBlockSubscription: Subscription;
|
||||
txConfirmedSubscription: Subscription;
|
||||
loadingBlocks$: Observable<boolean>;
|
||||
blockStyles = [];
|
||||
emptyBlockStyles = [];
|
||||
@@ -82,7 +84,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.chainTip = this.stateService.latestBlockHeight;
|
||||
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||
@@ -104,31 +105,22 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
if (!this.static) {
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block, txConfirmed]) => {
|
||||
if (this.blocks.some((b) => b.height === block.height)) {
|
||||
.subscribe((blocks) => {
|
||||
if (!blocks?.length) {
|
||||
return;
|
||||
}
|
||||
const latestHeight = blocks[0].height;
|
||||
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
||||
|
||||
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
|
||||
this.blocks = [];
|
||||
this.blocksFilled = false;
|
||||
for (const block of blocks) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
||||
|
||||
if (txConfirmed && block.height > this.chainTip) {
|
||||
this.markHeight = block.height;
|
||||
this.moveArrowToPosition(true, true);
|
||||
} else {
|
||||
this.moveArrowToPosition(true, false);
|
||||
}
|
||||
this.blocks = blocks;
|
||||
|
||||
this.blockStyles = [];
|
||||
if (this.blocksFilled && block.height > this.chainTip) {
|
||||
if (animate) {
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
|
||||
setTimeout(() => {
|
||||
this.blockStyles = [];
|
||||
@@ -139,13 +131,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||
}
|
||||
|
||||
if (this.blocks.length === this.dynamicBlocksAmount) {
|
||||
this.blocksFilled = true;
|
||||
}
|
||||
this.chainTip = latestHeight;
|
||||
|
||||
this.chainTip = Math.max(this.chainTip, block.height);
|
||||
if (this.pendingMarkBlock) {
|
||||
this.moveArrowToPosition(this.pendingMarkBlock.animate, this.pendingMarkBlock.newBlockFromLeft);
|
||||
this.pendingMarkBlock = null;
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txid, block]) => {
|
||||
if (txid) {
|
||||
this.markHeight = block.height;
|
||||
this.moveArrowToPosition(true, true);
|
||||
} else {
|
||||
this.moveArrowToPosition(true, false);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||
if (block.height <= this.height && block.height > this.height - this.count) {
|
||||
@@ -164,9 +166,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
if (this.static) {
|
||||
this.updateStaticBlocks();
|
||||
}
|
||||
if (this.static) {
|
||||
this.updateStaticBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
@@ -190,6 +192,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.blockPageSubscription) {
|
||||
this.blockPageSubscription.unsubscribe();
|
||||
}
|
||||
if (this.txConfirmedSubscription) {
|
||||
this.txConfirmedSubscription.unsubscribe();
|
||||
}
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.tabHiddenSubscription.unsubscribe();
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
@@ -202,6 +207,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.arrowVisible = false;
|
||||
return;
|
||||
}
|
||||
if (this.chainTip == null) {
|
||||
this.pendingMarkBlock = { animate, newBlockFromLeft };
|
||||
}
|
||||
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
||||
if (blockindex > -1) {
|
||||
if (!animate) {
|
||||
|
||||
@@ -82,12 +82,12 @@ export class BlocksList implements OnInit {
|
||||
),
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0].height <= this.lastBlockHeight) {
|
||||
switchMap((blocks) => {
|
||||
if (blocks[0].height <= this.lastBlockHeight) {
|
||||
return [null]; // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = block[0].height;
|
||||
return [block];
|
||||
this.lastBlockHeight = blocks[0].height;
|
||||
return blocks;
|
||||
})
|
||||
)
|
||||
])
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<div class="container-xl">
|
||||
<div class="text-center">
|
||||
<h2>Calculator</h2>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="price$ | async; else loading">
|
||||
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">{{ currency$ | async }}</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">BTC</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">sats</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
|
||||
<app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="bitcoin-satoshis-text">
|
||||
₿
|
||||
<span [innerHTML]="form.get('bitcoin').value | bitcoinsatoshis"></span>
|
||||
<span class="sats"> sats</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="fiat-text">
|
||||
<app-fiat [value]="form.get('satoshis').value" digitsInfo="1.0-0"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="symbol">
|
||||
Fiat price last updated <app-time kind="since" [time]="lastFiatPrice$ | async" [fastRender]="true"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="text-center">
|
||||
Waiting for price feed...
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
.input-group-text {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.bitcoin-satoshis-text {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.fiat-text {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.bitcoin-satoshis-text {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.sats {
|
||||
font-size: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: auto;
|
||||
}
|
||||
137
frontend/src/app/components/calculator/calculator.component.ts
Normal file
137
frontend/src/app/components/calculator/calculator.component.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-calculator',
|
||||
templateUrl: './calculator.component.html',
|
||||
styleUrls: ['./calculator.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CalculatorComponent implements OnInit {
|
||||
satoshis = 10000;
|
||||
form: FormGroup;
|
||||
|
||||
currency$ = this.stateService.fiatCurrency$;
|
||||
price$: Observable<number>;
|
||||
lastFiatPrice$: Observable<number>;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.formBuilder.group({
|
||||
fiat: [0],
|
||||
bitcoin: [0],
|
||||
satoshis: [0],
|
||||
});
|
||||
|
||||
this.lastFiatPrice$ = this.stateService.conversions$.asObservable()
|
||||
.pipe(
|
||||
map((conversions) => conversions.time)
|
||||
);
|
||||
|
||||
let currency;
|
||||
this.price$ = this.currency$.pipe(
|
||||
switchMap((result) => {
|
||||
currency = result;
|
||||
return this.stateService.conversions$.asObservable();
|
||||
}),
|
||||
map((conversions) => {
|
||||
return conversions[currency];
|
||||
})
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
this.price$,
|
||||
this.form.get('fiat').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
const rate = (value / price).toFixed(8);
|
||||
const satsRate = Math.round(value / price * 100_000_000);
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('bitcoin').setValue(rate, { emitEvent: false });
|
||||
this.form.get('satoshis').setValue(satsRate, { emitEvent: false } );
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
this.price$,
|
||||
this.form.get('bitcoin').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
const rate = parseFloat((value * price).toFixed(8));
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||
this.form.get('satoshis').setValue(Math.round(value * 100_000_000), { emitEvent: false } );
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
this.price$,
|
||||
this.form.get('satoshis').valueChanges
|
||||
]).subscribe(([price, value]) => {
|
||||
const rate = parseFloat((value / 100_000_000 * price).toFixed(8));
|
||||
const bitcoinRate = (value / 100_000_000).toFixed(8);
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
this.form.get('fiat').setValue(rate, { emitEvent: false } );
|
||||
this.form.get('bitcoin').setValue(bitcoinRate, { emitEvent: false });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
transformInput(name: string): void {
|
||||
const formControl = this.form.get(name);
|
||||
if (!formControl.value) {
|
||||
return formControl.setValue('', {emitEvent: false});
|
||||
}
|
||||
let value = formControl.value.replace(',', '.').replace(/[^0-9.]/g, '');
|
||||
if (value === '.') {
|
||||
value = '0';
|
||||
}
|
||||
let sanitizedValue = this.removeExtraDots(value);
|
||||
if (name === 'bitcoin' && this.countDecimals(sanitizedValue) > 8) {
|
||||
sanitizedValue = this.toFixedWithoutRounding(sanitizedValue, 8);
|
||||
}
|
||||
if (sanitizedValue === '') {
|
||||
sanitizedValue = '0';
|
||||
}
|
||||
if (name === 'satoshis') {
|
||||
sanitizedValue = parseFloat(sanitizedValue).toFixed(0);
|
||||
}
|
||||
formControl.setValue(sanitizedValue, {emitEvent: true});
|
||||
}
|
||||
|
||||
removeExtraDots(str: string): string {
|
||||
const [beforeDot, afterDot] = str.split('.', 2);
|
||||
if (afterDot === undefined) {
|
||||
return str;
|
||||
}
|
||||
const afterDotReplaced = afterDot.replace(/\./g, '');
|
||||
return `${beforeDot}.${afterDotReplaced}`;
|
||||
}
|
||||
|
||||
countDecimals(numberString: string): number {
|
||||
const decimalPos = numberString.indexOf('.');
|
||||
if (decimalPos === -1) return 0;
|
||||
return numberString.length - decimalPos - 1;
|
||||
}
|
||||
|
||||
toFixedWithoutRounding(numStr: string, fixed: number): string {
|
||||
const re = new RegExp(`^-?\\d+(?:.\\d{0,${(fixed || -1)}})?`);
|
||||
const result = numStr.match(re);
|
||||
return result ? result[0] : numStr;
|
||||
}
|
||||
|
||||
selectAll(event): void {
|
||||
event.target.select();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<span [style]="change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||
{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
|
||||
‎{{ change >= 0 ? '+' : '' }}{{ change | amountShortener }}%
|
||||
</span>
|
||||
|
||||
@@ -39,13 +39,10 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
||||
})
|
||||
).subscribe();
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block) {
|
||||
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
|
||||
// using block-reported times, so ensure they are sorted chronologically
|
||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||
this.updateSegments();
|
||||
}
|
||||
.subscribe((blocks) => {
|
||||
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
|
||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||
this.updateSegments();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</ng-container>
|
||||
<ng-template #mempoolMode>
|
||||
<div class="block-sizer" [style]="blockSizerStyle">
|
||||
<app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview>
|
||||
<app-mempool-block-overview [index]="blockIndex"></app-mempool-block-overview>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="fader"></div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
|
||||
--chain-height: 60px;
|
||||
--clock-width: 300px;
|
||||
|
||||
@@ -60,14 +60,11 @@ export class ClockComponent implements OnInit {
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block) {
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, 16);
|
||||
if (this.blocks[this.blockIndex]) {
|
||||
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
.subscribe((blocks) => {
|
||||
this.blocks = blocks.slice(0, 16);
|
||||
if (this.blocks[this.blockIndex]) {
|
||||
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -38,11 +38,12 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.difficultyEpoch$ = combineLatest([
|
||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
||||
this.stateService.blocks$,
|
||||
this.stateService.difficultyAdjustment$,
|
||||
])
|
||||
.pipe(
|
||||
map(([block, da]) => {
|
||||
map(([blocks, da]) => {
|
||||
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||
let colorAdjustments = '#ffffff66';
|
||||
if (da.difficultyChange > 0) {
|
||||
colorAdjustments = '#3bcc49';
|
||||
@@ -63,7 +64,7 @@ export class DifficultyMiningComponent implements OnInit {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
|
||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
|
||||
const data = {
|
||||
|
||||
@@ -67,11 +67,12 @@ export class DifficultyComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.difficultyEpoch$ = combineLatest([
|
||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
||||
this.stateService.blocks$,
|
||||
this.stateService.difficultyAdjustment$,
|
||||
])
|
||||
.pipe(
|
||||
map(([block, da]) => {
|
||||
map(([blocks, da]) => {
|
||||
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||
let colorAdjustments = '#ffffff66';
|
||||
if (da.difficultyChange > 0) {
|
||||
colorAdjustments = '#3bcc49';
|
||||
@@ -92,7 +93,7 @@ export class DifficultyComponent implements OnInit {
|
||||
colorPreviousAdjustments = '#ffffff66';
|
||||
}
|
||||
|
||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||
const blocksUntilHalving = 210000 - (maxHeight % 210000);
|
||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
|
||||
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
|
||||
|
||||
@@ -109,6 +109,14 @@ export class HashrateChartComponent implements OnInit {
|
||||
tap((response: any) => {
|
||||
const data = response.body;
|
||||
|
||||
// always include the latest difficulty
|
||||
if (data.difficulty.length && data.difficulty[data.difficulty.length - 1].difficulty !== data.currentDifficulty) {
|
||||
data.difficulty.push({
|
||||
timestamp: Date.now() / 1000,
|
||||
difficulty: data.currentDifficulty
|
||||
});
|
||||
}
|
||||
|
||||
// We generate duplicated data point so the tooltip works nicely
|
||||
const diffFixed = [];
|
||||
let diffIndex = 1;
|
||||
@@ -122,6 +130,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
});
|
||||
++hashIndex;
|
||||
}
|
||||
diffIndex++;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -137,6 +146,14 @@ export class HashrateChartComponent implements OnInit {
|
||||
++diffIndex;
|
||||
}
|
||||
|
||||
while (diffIndex <= data.difficulty.length) {
|
||||
diffFixed.push({
|
||||
timestamp: data.difficulty[diffIndex - 1].time,
|
||||
difficulty: data.difficulty[diffIndex - 1].difficulty
|
||||
});
|
||||
diffIndex++;
|
||||
}
|
||||
|
||||
let maResolution = 15;
|
||||
const hashrateMa = [];
|
||||
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</a>
|
||||
|
||||
<div ngbDropdown (window:resize)="onResize()" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'liquid' : network.val" width="20" height="20" viewBox="0 0 125 125"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -136,4 +136,19 @@ nav {
|
||||
}
|
||||
.navbar-dark .navbar-nav .nav-link {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
</a>
|
||||
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
|
||||
@@ -193,3 +193,18 @@ nav {
|
||||
font-size: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.current-network-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="isLoading$ | async"
|
||||
[resolution]="75"
|
||||
[resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="timeLtr ? 'right' : 'left'"
|
||||
[flip]="true"
|
||||
[pixelAlign]="pixelAlign"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Router } from '@angular/router';
|
||||
})
|
||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||
@Input() index: number;
|
||||
@Input() pixelAlign: boolean = false;
|
||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
@@ -124,7 +124,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => combineLatest([
|
||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
||||
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
|
||||
this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((mempoolBlocks) => {
|
||||
@@ -186,8 +186,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.blockSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
this.blockSubscription = this.stateService.blocks$.pipe(map((blocks) => blocks[0]))
|
||||
.subscribe((block) => {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
if (this.chainTip === -1) {
|
||||
this.animateEntry = block.height === this.stateService.latestBlockHeight;
|
||||
} else {
|
||||
@@ -221,8 +224,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
this.stateService.blocks$
|
||||
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
|
||||
.subscribe(([block]) => {
|
||||
.pipe(map((blocks) => blocks[0]))
|
||||
.subscribe((block) => {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
@@ -297,7 +300,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
while (blocks.length > blocksAmount) {
|
||||
const block = blocks.pop();
|
||||
if (!this.count) {
|
||||
const lastBlock = blocks[blocks.length - 1];
|
||||
const lastBlock = blocks[0];
|
||||
lastBlock.blockSize += block.blockSize;
|
||||
lastBlock.blockVSize += block.blockVSize;
|
||||
lastBlock.nTx += block.nTx;
|
||||
@@ -308,7 +311,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
if (blocks.length) {
|
||||
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
|
||||
blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export class PoolComponent implements OnInit {
|
||||
return this.apiService.getPoolStats$(slug);
|
||||
}),
|
||||
tap(() => {
|
||||
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
||||
this.loadMoreSubject.next(this.blocks[0]?.height);
|
||||
}),
|
||||
map((poolStats) => {
|
||||
this.seoService.setTitle(poolStats.pool.name);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1 class="float-left" i18n="page.rbf-replacements">RBF Replacements</h1>
|
||||
<div *ngIf="isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="mode-toggle float-right" *ngIf="fullRbfEnabled">
|
||||
<div class="mode-toggle float-right">
|
||||
<form class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" name="radioBasic">
|
||||
<label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
|
||||
@@ -22,12 +22,12 @@
|
||||
<div *ngFor="let tree of trees" class="tree">
|
||||
<p class="info">
|
||||
<span class="type">
|
||||
<span *ngIf="isMined(tree)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="isFullRbf(tree)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="tree.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="tree.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
</span>
|
||||
<app-time kind="since" [time]="tree.time"></app-time>
|
||||
</p>
|
||||
<div class="timeline-wrapper" [class.mined]="isMined(tree)">
|
||||
<div class="timeline-wrapper" [class.mined]="tree.mined">
|
||||
<app-rbf-timeline [replacements]="tree"></app-rbf-timeline>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,11 +36,6 @@
|
||||
<p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- <ngb-pagination class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="blocksCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
rbfTrees$: Observable<RbfTree[]>;
|
||||
nextRbfSubject = new BehaviorSubject(null);
|
||||
urlFragmentSubscription: Subscription;
|
||||
fullRbfEnabled: boolean;
|
||||
fullRbf: boolean;
|
||||
isLoading = true;
|
||||
|
||||
@@ -27,9 +26,7 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
) {
|
||||
this.fullRbfEnabled = stateService.env.FULL_RBF_ENABLED;
|
||||
}
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||
@@ -56,25 +53,6 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
toggleFullRbf(event) {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
fragment: this.fullRbf ? null : 'fullrbf'
|
||||
});
|
||||
}
|
||||
|
||||
isFullRbf(tree: RbfTree): boolean {
|
||||
return tree.fullRbf;
|
||||
}
|
||||
|
||||
isMined(tree: RbfTree): boolean {
|
||||
return tree.mined;
|
||||
}
|
||||
|
||||
// pageChange(page: number) {
|
||||
// this.fromTreeSubject.next(this.lastTreeId);
|
||||
// }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackRbf();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
|
||||
<td>
|
||||
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
|
||||
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
|
||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
||||
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||
import { RbfInfo } from '../../interfaces/node-api.interface';
|
||||
import { RbfTree } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rbf-timeline-tooltip',
|
||||
@@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface';
|
||||
styleUrls: ['./rbf-timeline-tooltip.component.scss'],
|
||||
})
|
||||
export class RbfTimelineTooltipComponent implements OnChanges {
|
||||
@Input() rbfInfo: RbfInfo | void;
|
||||
@Input() rbfInfo: RbfTree | null;
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
|
||||
tooltipPosition = null;
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<ng-container *ngIf="cell.replacement; else nonNode">
|
||||
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
|
||||
<div class="node"
|
||||
[id]="'node-'+cell.replacement.tx.txid"
|
||||
[class.selected]="txid === cell.replacement.tx.txid"
|
||||
[class.mined]="cell.replacement.tx.mined"
|
||||
[class.first-node]="cell.first"
|
||||
>
|
||||
<div class="track"></div>
|
||||
<div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
|
||||
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
|
||||
<a class="shape-border"
|
||||
[class.rbf]="cell.replacement.tx.rbf"
|
||||
[routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
|
||||
@@ -36,14 +37,14 @@
|
||||
</ng-container>
|
||||
<ng-template #nonNode>
|
||||
<ng-container [ngSwitch]="cell.connector">
|
||||
<div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div>
|
||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div>
|
||||
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="node-spacer" *ngSwitchDefault></div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
|
||||
<div class="track"></div>
|
||||
<div class="track" [class.fullrbf]="cell.fullRbf"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -83,15 +83,26 @@
|
||||
transform: translateY(-50%);
|
||||
background: #105fb0;
|
||||
border-radius: 5px;
|
||||
|
||||
&.left {
|
||||
right: 50%;
|
||||
}
|
||||
&.right {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
&.fullrbf {
|
||||
background: #1bd8f4;
|
||||
}
|
||||
}
|
||||
&.first-node {
|
||||
.track {
|
||||
left: 50%;
|
||||
.track.left {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
.track {
|
||||
right: 50%;
|
||||
.track.right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,11 +188,17 @@
|
||||
height: 108px;
|
||||
bottom: 50%;
|
||||
border-right: solid 10px #105fb0;
|
||||
&.fullrbf {
|
||||
border-right: solid 10px #1bd8f4;
|
||||
}
|
||||
}
|
||||
|
||||
.corner {
|
||||
border-bottom: solid 10px #105fb0;
|
||||
border-bottom-right-radius: 10px;
|
||||
&.fullrbf {
|
||||
border-bottom: solid 10px #1bd8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface';
|
||||
import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
type Connector = 'pipe' | 'corner';
|
||||
|
||||
interface TimelineCell {
|
||||
replacement?: RbfInfo,
|
||||
replacement?: RbfTree,
|
||||
connector?: Connector,
|
||||
first?: boolean,
|
||||
fullRbf?: boolean,
|
||||
}
|
||||
|
||||
function isTimelineCell(val: RbfTree | TimelineCell): boolean {
|
||||
return !val || !('tx' in val);
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() txid: string;
|
||||
rows: TimelineCell[][] = [];
|
||||
|
||||
hoverInfo: RbfInfo | void = null;
|
||||
hoverInfo: RbfTree | null = null;
|
||||
tooltipPosition = null;
|
||||
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
@@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
buildTimelines(tree: RbfTree): TimelineCell[][] {
|
||||
if (!tree) return [];
|
||||
|
||||
this.flagFullRbf(tree);
|
||||
const split = this.splitTimelines(tree);
|
||||
const timelines = this.prepareTimelines(split);
|
||||
return this.connectTimelines(timelines);
|
||||
}
|
||||
|
||||
// sets the fullRbf flag on each transaction in the tree
|
||||
flagFullRbf(tree: RbfTree): void {
|
||||
let fullRbf = false;
|
||||
for (const replaced of tree.replaces) {
|
||||
if (!replaced.tx.rbf) {
|
||||
fullRbf = true;
|
||||
}
|
||||
replaced.replacedBy = tree.tx;
|
||||
this.flagFullRbf(replaced);
|
||||
}
|
||||
tree.tx.fullRbf = fullRbf;
|
||||
}
|
||||
|
||||
// splits a tree into N leaf-to-root paths
|
||||
splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] {
|
||||
splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] {
|
||||
const replacements = [...tail, tree];
|
||||
if (tree.replaces.length) {
|
||||
return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
|
||||
@@ -70,7 +89,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
|
||||
// merges separate leaf-to-root paths into a coherent forking timeline
|
||||
// represented as a 2D array of Rbf events
|
||||
prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] {
|
||||
prepareTimelines(lines: RbfTree[][]): (RbfTree | TimelineCell)[][] {
|
||||
lines.sort((a, b) => b.length - a.length);
|
||||
|
||||
const rows = lines.map(() => []);
|
||||
@@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
let emptyCount = 0;
|
||||
const nextGroups = [];
|
||||
for (const group of lineGroups) {
|
||||
const toMerge: { [txid: string]: RbfInfo[][] } = {};
|
||||
const toMerge: { [txid: string]: RbfTree[][] } = {};
|
||||
let emptyInGroup = 0;
|
||||
let first = true;
|
||||
for (const line of group) {
|
||||
@@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
} else {
|
||||
// substitute duplicates with empty cells
|
||||
// (we'll fill these in with connecting lines later)
|
||||
rows[index].unshift(null);
|
||||
rows[index].unshift({ connector: true, replacement: head });
|
||||
}
|
||||
// group the tails of the remaining lines for the next iteration
|
||||
if (line.length) {
|
||||
@@ -127,7 +146,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
// annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
|
||||
connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] {
|
||||
connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] {
|
||||
const rows: TimelineCell[][] = [];
|
||||
timelines.forEach((lines, row) => {
|
||||
rows.push([]);
|
||||
@@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
let finished = false;
|
||||
lines.forEach((replacement, column) => {
|
||||
const cell: TimelineCell = {};
|
||||
if (replacement) {
|
||||
cell.replacement = replacement;
|
||||
if (!isTimelineCell(replacement)) {
|
||||
cell.replacement = replacement as RbfTree;
|
||||
cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf;
|
||||
}
|
||||
rows[row].push(cell);
|
||||
if (replacement) {
|
||||
if (!isTimelineCell(replacement)) {
|
||||
if (!started) {
|
||||
cell.first = true;
|
||||
started = true;
|
||||
@@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
matched = true;
|
||||
} else if (i === row) {
|
||||
rows[i][column] = {
|
||||
connector: 'corner'
|
||||
connector: 'corner',
|
||||
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
|
||||
};
|
||||
} else if (nextCell.connector !== 'corner') {
|
||||
rows[i][column] = {
|
||||
connector: 'pipe'
|
||||
connector: 'pipe',
|
||||
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,12 @@ export class RewardStatsComponent implements OnInit {
|
||||
// Or when we receive a newer block, newer than the latest reward stats api call
|
||||
this.stateService.blocks$
|
||||
.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0].height <= this.lastBlockHeight) {
|
||||
switchMap((blocks) => {
|
||||
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
|
||||
if (maxHeight <= this.lastBlockHeight) {
|
||||
return []; // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = block[0].height;
|
||||
this.lastBlockHeight = maxHeight;
|
||||
return this.apiService.getRewardStats$();
|
||||
})
|
||||
)
|
||||
|
||||
@@ -80,6 +80,9 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
tap((text) => {
|
||||
this.stateService.searchText$.next(text);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Inpu
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-start',
|
||||
@@ -55,8 +56,8 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => {
|
||||
this.blockCount++;
|
||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||
this.blockCount = blocks.length;
|
||||
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
|
||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
|
||||
@@ -110,9 +111,12 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
this.stateService.blocks$
|
||||
.subscribe((blocks: any) => {
|
||||
.subscribe((blocks: BlockExtended[]) => {
|
||||
this.countdown = 0;
|
||||
const block = blocks[0];
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const sb in specialBlocks) {
|
||||
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<div class="container-buttons">
|
||||
<app-confirmations
|
||||
*ngIf="tx"
|
||||
[chainTip]="latestBlock?.height"
|
||||
[height]="tx?.status?.block_height"
|
||||
[replaced]="replaced"
|
||||
@@ -306,7 +307,7 @@
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingTx && !error">
|
||||
<ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
@@ -451,7 +452,7 @@
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<ng-template [ngIf]="error && !loadingCachedTx">
|
||||
|
||||
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
|
||||
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs';
|
||||
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
@@ -39,6 +39,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
isLoadingTx = true;
|
||||
error: any = undefined;
|
||||
errorUnblinded: any = undefined;
|
||||
loadingCachedTx = false;
|
||||
waitingForTransaction = false;
|
||||
latestBlock: BlockExtended;
|
||||
transactionTime = -1;
|
||||
@@ -49,10 +50,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txReplacedSubscription: Subscription;
|
||||
txRbfInfoSubscription: Subscription;
|
||||
mempoolPositionSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
replaced: boolean = false;
|
||||
@@ -131,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
||||
});
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||
this.latestBlock = blocks[0];
|
||||
});
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
@@ -199,6 +204,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.fetchCachedTxSubscription = this.fetchCachedTx$
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.loadingCachedTx = true;
|
||||
}),
|
||||
switchMap((txId) =>
|
||||
this.apiService
|
||||
.getRbfCachedTx$(txId)
|
||||
@@ -207,6 +215,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe((tx) => {
|
||||
this.loadingCachedTx = false;
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
@@ -338,6 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
this.loadingCachedTx = false;
|
||||
this.waitingForTransaction = false;
|
||||
this.websocketService.startTrackTransaction(tx.txid);
|
||||
this.graphExpanded = false;
|
||||
@@ -369,7 +379,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ancestors: tx.ancestors,
|
||||
bestDescendant: tx.bestDescendant,
|
||||
};
|
||||
const hasRelatives = !!(tx.ancestors.length || tx.bestDescendant);
|
||||
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
|
||||
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
@@ -391,9 +401,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
||||
this.latestBlock = block;
|
||||
|
||||
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
@@ -409,6 +417,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
|
||||
if (!this.tx) {
|
||||
this.error = new Error();
|
||||
this.loadingCachedTx = false;
|
||||
this.waitingForTransaction = false;
|
||||
}
|
||||
this.rbfTransaction = rbfTransaction;
|
||||
@@ -593,13 +602,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.fetchCachedTxSubscription.unsubscribe();
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.txRbfInfoSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.flowPrefSubscription.unsubscribe();
|
||||
this.urlFragmentSubscription.unsubscribe();
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
this.mempoolPositionSubscription.unsubscribe();
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -56,7 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
|
||||
Reference in New Issue
Block a user