Merge branch 'master' into nymkappa/clip-label-overflow
This commit is contained in:
@@ -22,6 +22,7 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
import { CalculatorComponent } from './components/calculator/calculator.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -278,6 +279,10 @@ let routes: Routes = [
|
||||
path: 'rbf',
|
||||
component: RbfList,
|
||||
},
|
||||
{
|
||||
path: 'tools/calculator',
|
||||
component: CalculatorComponent
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -48,8 +48,7 @@ const providers = [
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
BrowserTransferStateModule,
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
|
||||
@@ -112,7 +112,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
this.error = error;
|
||||
});
|
||||
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block)));
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -75,36 +75,31 @@
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-blocks-table">
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th>
|
||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
|
||||
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
|
||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
<tr *ngFor="let replacement of replacements$ | async;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||
<td class="table-cell-size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-badges">
|
||||
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -185,10 +180,10 @@
|
||||
<ng-template #mempoolTable let-mempoolInfoData>
|
||||
<div class="mempool-info-data">
|
||||
<div class="item">
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.00001 || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee > 0.00001">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@@ -175,39 +175,43 @@
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
.lastest-replacements-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 15%;
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-mined {
|
||||
width: 35%;
|
||||
text-align: left;
|
||||
.table-cell-txid {
|
||||
width: 25%;
|
||||
text-align: start;
|
||||
}
|
||||
.table-cell-transaction-count {
|
||||
display: none;
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
display: table-cell;
|
||||
}
|
||||
.table-cell-size {
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.table-cell-old-fee {
|
||||
width: 25%;
|
||||
text-align: end;
|
||||
|
||||
@media(max-width: 1080px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
.table-cell-new-fee {
|
||||
width: 20%;
|
||||
text-align: end;
|
||||
}
|
||||
.table-cell-badges {
|
||||
width: 23%;
|
||||
padding-right: 0;
|
||||
padding-left: 5px;
|
||||
text-align: end;
|
||||
|
||||
.badge {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { filter, map, scan, share, switchMap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { WebsocketService } from '../services/websocket.service';
|
||||
@@ -38,8 +38,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
replacements$: Observable<ReplacementInfo[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
mempoolStats$: Observable<MempoolStatsData>;
|
||||
@@ -58,12 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.seoService.resetTitle();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.websocketService.startTrackRbfSummary();
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||
.pipe(
|
||||
@@ -130,30 +132,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
this.blocks$ = this.stateService.blocks$
|
||||
.pipe(
|
||||
tap(([block]) => {
|
||||
this.latestBlockHeight = block.height;
|
||||
}),
|
||||
scan((acc, [block]) => {
|
||||
if (acc.find((b) => b.height == block.height)) {
|
||||
return acc;
|
||||
}
|
||||
acc.unshift(block);
|
||||
acc = acc.slice(0, 6);
|
||||
|
||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
||||
for (const block of acc) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
);
|
||||
|
||||
this.transactions$ = this.stateService.transactions$
|
||||
.pipe(
|
||||
scan((acc, tx) => {
|
||||
@@ -166,6 +144,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}, []),
|
||||
);
|
||||
|
||||
this.replacements$ = this.stateService.rbfLatestSummary$;
|
||||
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
</span>
|
||||
|
||||
<ng-template #noblockconversion>
|
||||
<span [class]="colorClass" *ngIf="(conversions$ | async) as conversions">
|
||||
<span [class]="colorClass" *ngIf="(conversions$ | async) as conversions; else noconversion">
|
||||
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
|
||||
</span>
|
||||
<ng-template #noconversion>
|
||||
<span> </span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo {
|
||||
mined?: boolean;
|
||||
fullRbf: boolean;
|
||||
replaces: RbfTree[];
|
||||
replacedBy?: RbfTransaction;
|
||||
}
|
||||
|
||||
export interface DifficultyAdjustment {
|
||||
@@ -172,13 +173,15 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
interface RbfTransaction extends TransactionStripped {
|
||||
export interface RbfTransaction extends TransactionStripped {
|
||||
rbf?: boolean;
|
||||
mined?: boolean,
|
||||
fullRbf?: boolean,
|
||||
}
|
||||
export interface MempoolPosition {
|
||||
block: number,
|
||||
@@ -267,6 +270,7 @@ export interface IChannel {
|
||||
closing_transaction_id: string;
|
||||
closing_reason: string;
|
||||
updated_at: string;
|
||||
closing_date?: string;
|
||||
created: string;
|
||||
status: number;
|
||||
node_left: INode,
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface WebsocketResponse {
|
||||
txReplaced?: ReplacedTransaction;
|
||||
rbfInfo?: RbfTree;
|
||||
rbfLatest?: RbfTree[];
|
||||
rbfLatestSummary?: ReplacementInfo[];
|
||||
utxoSpent?: object;
|
||||
transactions?: TransactionStripped[];
|
||||
loadingIndicators?: ILoadingIndicators;
|
||||
@@ -29,6 +30,7 @@ export interface WebsocketResponse {
|
||||
'track-asset'?: string;
|
||||
'track-mempool-block'?: number;
|
||||
'track-rbf'?: string;
|
||||
'track-rbf-summary'?: boolean;
|
||||
'watch-mempool'?: boolean;
|
||||
'track-bisq-market'?: string;
|
||||
'refresh-blocks'?: boolean;
|
||||
@@ -37,6 +39,16 @@ export interface WebsocketResponse {
|
||||
export interface ReplacedTransaction extends Transaction {
|
||||
txid: string;
|
||||
}
|
||||
|
||||
export interface ReplacementInfo {
|
||||
mined: boolean;
|
||||
fullRbf: boolean;
|
||||
txid: string;
|
||||
oldFee: number;
|
||||
oldVsize: number;
|
||||
newFee: number;
|
||||
newVsize: number;
|
||||
}
|
||||
export interface MempoolBlock {
|
||||
blink?: boolean;
|
||||
height?: number;
|
||||
@@ -77,7 +89,7 @@ export interface TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="col-md table-col">
|
||||
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
.table-col {
|
||||
max-width: calc(100% - 470px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 32px;
|
||||
margin-top: 10px;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h1 class="float-left" i18n="lightning.liquidity-ranking">Penalties</h1>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="timestamp" i18n="lightning.closed-at">Closed</th>
|
||||
<th class="channels text-right" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="node text-right"></th>
|
||||
<th class="node text-right" i18n="lightning.node">Nodes</th>
|
||||
<th class="channelid text-right" i18n="channels.id">Channel ID</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody *ngIf="justiceChannels$ | async as channels">
|
||||
<ng-container *ngFor="let channel of channels;">
|
||||
<tr>
|
||||
<td class="timestamp">
|
||||
‎{{ channel.closing_date | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<app-amount *ngIf="channel.capacity > 100000000; else smallnode" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ channel.capacity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="alias text-right">
|
||||
<app-truncate [text]="channel.alias_left || '?'" [maxWidth]="200" [lastChars]="6" textAlign="end" [inline]="true"></app-truncate>
|
||||
</td>
|
||||
<td class="alias text-right">
|
||||
<app-truncate [text]="channel.alias_right || '?'" [maxWidth]="200" [lastChars]="6" textAlign="end" [inline]="true"></app-truncate>
|
||||
</td>
|
||||
<td class="channelid text-right">
|
||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="toggleDetails(channel)"
|
||||
i18n="transaction.details|Transaction Details">Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="channel.short_id === expanded">
|
||||
<ng-container *ngTemplateOutlet="channelTransactions"></ng-container>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #channelTransactions>
|
||||
<td colspan="6" *ngIf="transactions && !loadingTransactions else loadingTemplate;">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<div class="d-flex">
|
||||
<h5 i18n="lightning.opening-transaction">Opening transaction</h5>
|
||||
</div>
|
||||
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
|
||||
</app-transactions-list>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header d-flex">
|
||||
<h5 style="margin: 0;" i18n="lightning.closing-transaction">Closing transaction</h5> <app-closing-type [type]="3"></app-closing-type>
|
||||
</div>
|
||||
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
|
||||
</app-transactions-list>
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingTemplate>
|
||||
<td colspan="6">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,52 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
.container-xl.widget {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pool {
|
||||
width: 15%;
|
||||
@media (max-width: 575px) {
|
||||
width: 75%;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
}
|
||||
.pool-name {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.liquidity {
|
||||
width: 10%;
|
||||
@media (max-width: 575px) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat {
|
||||
width: 15%;
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
display: none !important;
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { map, Observable, of, Subject, Subscription, switchMap, tap, zip } from 'rxjs';
|
||||
import { IChannel } from '../../interfaces/node-api.interface';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-justice-list',
|
||||
templateUrl: './justice-list.component.html',
|
||||
styleUrls: ['./justice-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class JusticeList implements OnInit, OnDestroy {
|
||||
justiceChannels$: Observable<any[]>;
|
||||
fetchTransactions$: Subject<IChannel> = new Subject();
|
||||
transactionsSubscription: Subscription;
|
||||
transactions: Transaction[];
|
||||
expanded: string = null;
|
||||
loadingTransactions: boolean = true;
|
||||
|
||||
constructor(
|
||||
private apiService: LightningApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.justiceChannels$ = this.apiService.getPenaltyClosedChannels$();
|
||||
|
||||
this.transactionsSubscription = this.fetchTransactions$.pipe(
|
||||
tap(() => {
|
||||
this.loadingTransactions = true;
|
||||
}),
|
||||
switchMap((channel: IChannel) => {
|
||||
return zip([
|
||||
channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
|
||||
channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id) : of(null),
|
||||
]);
|
||||
}),
|
||||
).subscribe((transactions) => {
|
||||
this.transactions = transactions;
|
||||
this.loadingTransactions = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
toggleDetails(channel: any): void {
|
||||
if (this.expanded === channel.short_id) {
|
||||
this.expanded = null;
|
||||
} else {
|
||||
this.expanded = channel.short_id;
|
||||
this.fetchTransactions$.next(channel);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.transactionsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||
import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -84,6 +84,12 @@ export class LightningApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getPenaltyClosedChannels$(): Observable<IChannel[]> {
|
||||
return this.httpClient.get<IChannel[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/penalties'
|
||||
);
|
||||
}
|
||||
|
||||
getOldestNodes$(): Observable<IOldestNodes[]> {
|
||||
return this.httpClient.get<IOldestNodes[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
|
||||
@@ -29,6 +29,7 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
||||
import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component';
|
||||
import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component';
|
||||
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
|
||||
import { JusticeList } from '../lightning/justice-list/justice-list.component';
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||
@@ -60,6 +61,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesRanking,
|
||||
TopNodesPerChannels,
|
||||
TopNodesPerCapacity,
|
||||
JusticeList,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
@@ -97,6 +99,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesRanking,
|
||||
TopNodesPerChannels,
|
||||
TopNodesPerCapacity,
|
||||
JusticeList,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
||||
import { NodesRanking } from './nodes-ranking/nodes-ranking.component';
|
||||
import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { GroupComponent } from './group/group.component';
|
||||
import { JusticeList } from './justice-list/justice-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -66,6 +67,10 @@ const routes: Routes = [
|
||||
type: 'oldest'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'penalties',
|
||||
component: JusticeList,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
.table-col {
|
||||
max-width: calc(100% - 470px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 6px;
|
||||
font-size: 32px;
|
||||
@@ -18,10 +23,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-col {
|
||||
max-width: calc(100% - 470px);
|
||||
}
|
||||
|
||||
.map-col {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
</div>
|
||||
|
||||
<div class="box" *ngIf="!error">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
@@ -59,6 +58,9 @@
|
||||
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
|
||||
<td class="direction-ltr">{{ avgDistance | amountShortener: 1 }} <span class="symbol">km</span> <span class="separator">·</span>{{ kmToMiles(avgDistance) | amountShortener: 1 }} <span class="symbol">mi</span></td>
|
||||
</tr>
|
||||
<tr *ngIf="!node.geolocation" class="d-none d-md-table-row">
|
||||
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -100,11 +102,50 @@
|
||||
</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
<tr *ngIf="node.geolocation && node.featuresBits">
|
||||
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||
</tr>
|
||||
<tr *ngIf="!node.geolocation && node.featuresBits" class="d-table-row d-md-none">
|
||||
<ng-container *ngTemplateOutlet="featurebits;context:{bits: node.featuresBits}"></ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #featurebits let-bits="bits">
|
||||
<td i18n="lightning.features" class="text-truncate label">Features</td>
|
||||
<td class="d-flex justify-content-between">
|
||||
<span class="text-truncate w-90">{{ bits }}</span>
|
||||
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<div class="box mt-2" *ngIf="!error && showFeatures">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<h5>Raw bits</h5>
|
||||
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
|
||||
</div>
|
||||
<h5>Decoded</h5>
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<thead>
|
||||
<th style="width: 13%">Bit</th>
|
||||
<th>Name</th>
|
||||
<th style="width: 25%; text-align: right">Required</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let feature of node.features">
|
||||
<td style="width: 13%">{{ feature.bit }}</td>
|
||||
<td>{{ feature.name }}</td>
|
||||
<td style="width: 25%; text-align: right">{{ feature.is_required }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">
|
||||
|
||||
@@ -37,7 +37,7 @@ export class NodeComponent implements OnInit {
|
||||
liquidityAd: ILiquidityAd;
|
||||
tlvRecords: CustomRecord[];
|
||||
avgChannelDistance$: Observable<number | null>;
|
||||
|
||||
showFeatures = false;
|
||||
kmToMiles = kmToMiles;
|
||||
|
||||
constructor(
|
||||
@@ -164,4 +164,9 @@ export class NodeComponent implements OnInit {
|
||||
onLoadingEvent(e) {
|
||||
this.channelListLoading = e;
|
||||
}
|
||||
|
||||
toggleFeatures() {
|
||||
this.showFeatures = !this.showFeatures;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
<div class="container-xl" style="min-height: 335px" [ngClass]="{'widget': widget, 'full-height': !widget}">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.liquidity-ranking">Liquidity Ranking</h1>
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.connectivity-ranking">Connectivity Ranking</h1>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
@@ -22,6 +23,7 @@ export class TopNodesPerChannels implements OnInit {
|
||||
constructor(
|
||||
private apiService: LightningApiService,
|
||||
private stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -32,6 +34,8 @@ export class TopNodesPerChannels implements OnInit {
|
||||
}
|
||||
|
||||
if (this.widget === false) {
|
||||
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
|
||||
|
||||
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
|
||||
map((ranking) => {
|
||||
for (const i in ranking) {
|
||||
|
||||
@@ -18,6 +18,7 @@ export class CacheService {
|
||||
txCache: { [txid: string]: Transaction } = {};
|
||||
|
||||
network: string;
|
||||
blockHashCache: { [hash: string]: BlockExtended } = {};
|
||||
blockCache: { [height: number]: BlockExtended } = {};
|
||||
blockLoading: { [height: number]: boolean } = {};
|
||||
copiesInBlockQueue: { [height: number]: number } = {};
|
||||
@@ -27,8 +28,10 @@ export class CacheService {
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
) {
|
||||
this.stateService.blocks$.subscribe(([block]) => {
|
||||
this.addBlockToCache(block);
|
||||
this.stateService.blocks$.subscribe((blocks) => {
|
||||
for (const block of blocks) {
|
||||
this.addBlockToCache(block);
|
||||
}
|
||||
this.clearBlocks();
|
||||
});
|
||||
this.stateService.chainTip$.subscribe((height) => {
|
||||
@@ -56,8 +59,11 @@ export class CacheService {
|
||||
}
|
||||
|
||||
addBlockToCache(block: BlockExtended) {
|
||||
this.blockCache[block.height] = block;
|
||||
this.bumpBlockPriority(block.height);
|
||||
if (!this.blockHashCache[block.id]) {
|
||||
this.blockHashCache[block.id] = block;
|
||||
this.blockCache[block.height] = block;
|
||||
this.bumpBlockPriority(block.height);
|
||||
}
|
||||
}
|
||||
|
||||
async loadBlock(height) {
|
||||
@@ -105,7 +111,9 @@ export class CacheService {
|
||||
} else if ((this.tip - height) < KEEP_RECENT_BLOCKS) {
|
||||
this.bumpBlockPriority(height);
|
||||
} else {
|
||||
const block = this.blockCache[height];
|
||||
delete this.blockCache[height];
|
||||
delete this.blockHashCache[block.id];
|
||||
delete this.copiesInBlockQueue[height];
|
||||
}
|
||||
}
|
||||
@@ -113,6 +121,7 @@ export class CacheService {
|
||||
|
||||
// remove all blocks from the cache
|
||||
resetBlockCache() {
|
||||
this.blockHashCache = {};
|
||||
this.blockCache = {};
|
||||
this.blockLoading = {};
|
||||
this.copiesInBlockQueue = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
import { StateService } from './state.service';
|
||||
@@ -65,12 +65,12 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||
}
|
||||
|
||||
getAddressTransactions$(address: string): Observable<Transaction[]> {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
|
||||
}
|
||||
|
||||
getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
|
||||
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||
}
|
||||
|
||||
getAsset$(assetId: string): Observable<Asset> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { map, scan, shareReplay, tap } from 'rxjs/operators';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
export interface MarkBlockState {
|
||||
@@ -45,7 +45,6 @@ export interface Env {
|
||||
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
FULL_RBF_ENABLED: boolean;
|
||||
HISTORICAL_PRICE: boolean;
|
||||
}
|
||||
|
||||
@@ -76,7 +75,6 @@ const defaultEnv: Env = {
|
||||
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'FULL_RBF_ENABLED': false,
|
||||
'HISTORICAL_PRICE': true,
|
||||
};
|
||||
|
||||
@@ -90,10 +88,12 @@ export class StateService {
|
||||
blockVSize: number;
|
||||
env: Env;
|
||||
latestBlockHeight = -1;
|
||||
blocks: BlockExtended[] = [];
|
||||
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
lightningChanged$ = new ReplaySubject<boolean>(1);
|
||||
blocks$: ReplaySubject<[BlockExtended, string]>;
|
||||
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||
conversions$ = new ReplaySubject<any>(1);
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
@@ -102,9 +102,11 @@ export class StateService {
|
||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||
txRbfInfo$ = new Subject<RbfTree>();
|
||||
rbfLatest$ = new Subject<RbfTree[]>();
|
||||
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
|
||||
utxoSpent$ = new Subject<object>();
|
||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||
mempoolTransactions$ = new Subject<Transaction>();
|
||||
@@ -126,6 +128,7 @@ export class StateService {
|
||||
|
||||
markBlock$ = new BehaviorSubject<MarkBlockState>({});
|
||||
keyNavigation$ = new Subject<KeyboardEvent>();
|
||||
searchText$ = new BehaviorSubject<string>('');
|
||||
|
||||
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
||||
resetScroll$: Subject<boolean> = new Subject<boolean>();
|
||||
@@ -167,8 +170,6 @@ export class StateService {
|
||||
}
|
||||
});
|
||||
|
||||
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
this.liveMempoolBlockTransactions$ = merge(
|
||||
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
|
||||
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
|
||||
@@ -198,8 +199,15 @@ export class StateService {
|
||||
this.networkChanged$.next(this.env.BASE_MODULE);
|
||||
}
|
||||
|
||||
this.networkChanged$.subscribe((network) => {
|
||||
this.transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||
this.blocksSubject$.next([]);
|
||||
});
|
||||
|
||||
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
||||
|
||||
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
|
||||
|
||||
const savedTimePreference = this.storageService.getValue('time-preference-ltr');
|
||||
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
|
||||
// default time direction is right-to-left, unless locale is a RTL language
|
||||
@@ -336,4 +344,15 @@ export class StateService {
|
||||
this.chainTip$.next(height);
|
||||
}
|
||||
}
|
||||
|
||||
resetBlocks(blocks: BlockExtended[]): void {
|
||||
this.blocks = blocks.reverse();
|
||||
this.blocksSubject$.next(blocks);
|
||||
}
|
||||
|
||||
addBlock(block: BlockExtended): void {
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
||||
this.blocksSubject$.next(this.blocks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
const OFFLINE_RETRY_AFTER_MS = 2000;
|
||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
|
||||
@@ -29,6 +29,7 @@ export class WebsocketService {
|
||||
private trackingTxId: string;
|
||||
private isTrackingMempoolBlock = false;
|
||||
private isTrackingRbf = false;
|
||||
private isTrackingRbfSummary = false;
|
||||
private trackingMempoolBlock: number;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
@@ -40,6 +41,7 @@ export class WebsocketService {
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private transferState: TransferState,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
if (!this.stateService.isBrowser) {
|
||||
// @ts-ignore
|
||||
@@ -184,6 +186,16 @@ export class WebsocketService {
|
||||
this.isTrackingRbf = false;
|
||||
}
|
||||
|
||||
startTrackRbfSummary() {
|
||||
this.websocketSubject.next({ 'track-rbf-summary': true });
|
||||
this.isTrackingRbfSummary = true;
|
||||
}
|
||||
|
||||
stopTrackRbfSummary() {
|
||||
this.websocketSubject.next({ 'track-rbf-summary': false });
|
||||
this.isTrackingRbfSummary = false;
|
||||
}
|
||||
|
||||
startTrackBisqMarket(market: string) {
|
||||
this.websocketSubject.next({ 'track-bisq-market': market });
|
||||
}
|
||||
@@ -239,13 +251,8 @@ export class WebsocketService {
|
||||
|
||||
if (response.blocks && response.blocks.length) {
|
||||
const blocks = response.blocks;
|
||||
let maxHeight = 0;
|
||||
blocks.forEach((block: BlockExtended) => {
|
||||
if (block.height > this.stateService.latestBlockHeight) {
|
||||
maxHeight = Math.max(maxHeight, block.height);
|
||||
this.stateService.blocks$.next([block, '']);
|
||||
}
|
||||
});
|
||||
this.stateService.resetBlocks(blocks);
|
||||
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight);
|
||||
this.stateService.updateChainTip(maxHeight);
|
||||
}
|
||||
|
||||
@@ -260,7 +267,8 @@ export class WebsocketService {
|
||||
if (response.block) {
|
||||
if (response.block.height === this.stateService.latestBlockHeight + 1) {
|
||||
this.stateService.updateChainTip(response.block.height);
|
||||
this.stateService.blocks$.next([response.block, response.txConfirmed || '']);
|
||||
this.stateService.addBlock(response.block);
|
||||
this.stateService.txConfirmed$.next([response.txConfirmed, response.block]);
|
||||
} else if (response.block.height > this.stateService.latestBlockHeight + 1) {
|
||||
reinitBlocks = true;
|
||||
}
|
||||
@@ -286,6 +294,10 @@ export class WebsocketService {
|
||||
this.stateService.rbfLatest$.next(response.rbfLatest);
|
||||
}
|
||||
|
||||
if (response.rbfLatestSummary) {
|
||||
this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary);
|
||||
}
|
||||
|
||||
if (response.txReplaced) {
|
||||
this.stateService.txReplaced$.next(response.txReplaced);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!confirmations && height != null">
|
||||
<button type="button" class="btn btn-sm btn-success {{buttonClass}}" i18n="transaction.confirmed|Transaction confirmed state">Confirmed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && !removed">
|
||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
@@ -1,4 +1,4 @@
|
||||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null">
|
||||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
|
||||
<ng-container *ngIf="link">
|
||||
<a [routerLink]="link" class="truncate-link">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
|
||||
@@ -23,4 +23,8 @@
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ export class TruncateComponent {
|
||||
@Input() link: any = null;
|
||||
@Input() lastChars: number = 4;
|
||||
@Input() maxWidth: number = null;
|
||||
@Input() inline: boolean = false;
|
||||
@Input() textAlign: 'start' | 'end' = 'start';
|
||||
rtl: boolean;
|
||||
|
||||
constructor(
|
||||
|
||||
28
frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts
Normal file
28
frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'bitcoinsatoshis'
|
||||
})
|
||||
export class BitcoinsatoshisPipe implements PipeTransform {
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) { }
|
||||
|
||||
transform(value: string): SafeHtml {
|
||||
const newValue = this.insertSpaces(parseFloat(value || '0').toFixed(8));
|
||||
const position = (newValue || '0').search(/[1-9]/);
|
||||
|
||||
const firstPart = newValue.slice(0, position);
|
||||
const secondPart = newValue.slice(position);
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(
|
||||
`<span class="text-secondary">${firstPart}</span>${secondPart}`
|
||||
);
|
||||
}
|
||||
|
||||
insertSpaces(str: string): string {
|
||||
const length = str.length;
|
||||
return str.slice(0, length - 6) + ' ' + str.slice(length - 6, length - 3) + ' ' + str.slice(length - 3);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -97,6 +97,8 @@ import { MempoolBlockOverviewComponent } from '../components/mempool-block-overv
|
||||
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
|
||||
import { ClockFaceComponent } from '../components/clock-face/clock-face.component';
|
||||
import { ClockComponent } from '../components/clock/clock.component';
|
||||
import { CalculatorComponent } from '../components/calculator/calculator.component';
|
||||
import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe';
|
||||
|
||||
import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
|
||||
|
||||
@@ -185,12 +187,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
GeolocationComponent,
|
||||
TestnetAlertComponent,
|
||||
GlobalFooterComponent,
|
||||
|
||||
CalculatorComponent,
|
||||
BitcoinsatoshisPipe,
|
||||
MempoolBlockOverviewComponent,
|
||||
ClockchainComponent,
|
||||
ClockComponent,
|
||||
ClockFaceComponent,
|
||||
|
||||
OnlyVsizeDirective,
|
||||
OnlyWeightDirective
|
||||
],
|
||||
|
||||
@@ -1,69 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.2" width="135.73mm" height="135.73mm" viewBox="0 0 13573 13573" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" xmlns:ooo="http://xml.openoffice.org/svg/export" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:presentation="http://sun.com/xmlns/staroffice/presentation" xmlns:smil="http://www.w3.org/2001/SMIL20/" xmlns:anim="urn:oasis:names:tc:opendocument:xmlns:animation:1.0" xml:space="preserve">
|
||||
<defs class="ClipPathGroup">
|
||||
<clipPath id="presentation_clip_path" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="13573" height="13573"/>
|
||||
</clipPath>
|
||||
<clipPath id="presentation_clip_path_shrink" clipPathUnits="userSpaceOnUse">
|
||||
<rect x="13" y="13" width="13546" height="13546"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<defs class="TextShapeIndex">
|
||||
<g ooo:slide="id1" ooo:id-list="id3"/>
|
||||
</defs>
|
||||
<defs class="EmbeddedBulletChars">
|
||||
<g id="bullet-char-template-57356" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 580,1141 L 1163,571 580,0 -4,571 580,1141 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-57354" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 8,1128 L 1137,1128 1137,0 8,0 8,1128 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-10146" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 174,0 L 602,739 174,1481 1456,739 174,0 Z M 1358,739 L 309,1346 659,739 1358,739 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-10132" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 2015,739 L 1276,0 717,0 1260,543 174,543 174,936 1260,936 717,1481 1274,1481 2015,739 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-10007" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 0,-2 C -7,14 -16,27 -25,37 L 356,567 C 262,823 215,952 215,954 215,979 228,992 255,992 264,992 276,990 289,987 310,991 331,999 354,1012 L 381,999 492,748 772,1049 836,1024 860,1049 C 881,1039 901,1025 922,1006 886,937 835,863 770,784 769,783 710,716 594,584 L 774,223 C 774,196 753,168 711,139 L 727,119 C 717,90 699,76 672,76 641,76 570,178 457,381 L 164,-76 C 142,-110 111,-127 72,-127 30,-127 9,-110 8,-76 1,-67 -2,-52 -2,-32 -2,-23 -1,-13 0,-2 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-10004" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 285,-33 C 182,-33 111,30 74,156 52,228 41,333 41,471 41,549 55,616 82,672 116,743 169,778 240,778 293,778 328,747 346,684 L 369,508 C 377,444 397,411 428,410 L 1163,1116 C 1174,1127 1196,1133 1229,1133 1271,1133 1292,1118 1292,1087 L 1292,965 C 1292,929 1282,901 1262,881 L 442,47 C 390,-6 338,-33 285,-33 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-9679" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 813,0 C 632,0 489,54 383,161 276,268 223,411 223,592 223,773 276,916 383,1023 489,1130 632,1184 813,1184 992,1184 1136,1130 1245,1023 1353,916 1407,772 1407,592 1407,412 1353,268 1245,161 1136,54 992,0 813,0 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-8226" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 346,457 C 273,457 209,483 155,535 101,586 74,649 74,723 74,796 101,859 155,911 209,963 273,989 346,989 419,989 480,963 531,910 582,859 608,796 608,723 608,648 583,586 532,535 482,483 420,457 346,457 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-8211" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M -4,459 L 1135,459 1135,606 -4,606 -4,459 Z"/>
|
||||
</g>
|
||||
<g id="bullet-char-template-61548" transform="scale(0.00048828125,-0.00048828125)">
|
||||
<path d="M 173,740 C 173,903 231,1043 346,1159 462,1274 601,1332 765,1332 928,1332 1067,1274 1183,1159 1299,1043 1357,903 1357,740 1357,577 1299,437 1183,322 1067,206 928,148 765,148 601,148 462,206 346,322 231,437 173,577 173,740 Z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<g>
|
||||
<g id="id2" class="Master_Slide">
|
||||
<g id="bg-id2" class="Background"/>
|
||||
<g id="bo-id2" class="BackgroundObjects"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="SlideGroup">
|
||||
<g>
|
||||
<g id="container-id1">
|
||||
<g id="id1" class="Slide" clip-path="url(#presentation_clip_path)">
|
||||
<g class="Page">
|
||||
<g class="com.sun.star.drawing.ClosedBezierShape">
|
||||
<g id="id3">
|
||||
<rect class="BoundingBox" stroke="none" fill="none" x="681" y="481" width="12413" height="12571"/>
|
||||
<path fill="rgb(178,178,178)" stroke="none" d="M 3025,482 C 2802,483 2580,504 2361,546 5189,2249 7300,4524 8967,7155 9034,5734 8462,4269 7551,3076 7178,3216 6719,3095 6402,2778 6085,2461 5964,2001 6103,1629 5158,916 4079,477 3025,482 Z M 11216,3076 L 12011,6397 10553,6630 10040,8762 11984,9797 10893,11277 11678,12442 9329,11711 9765,10551 7737,9655 8084,7418 5138,8956 5027,11026 2058,10295 1178,13050 13092,13050 13092,1022 11216,3076 Z M 6921,1567 C 6911,1567 6901,1567 6891,1568 6794,1577 6710,1613 6649,1674 6486,1837 6497,2174 6751,2428 7005,2683 7342,2693 7504,2531 7667,2368 7656,2031 7402,1777 7253,1628 7075,1562 6921,1567 Z M 5212,3389 L 682,7919 C 795,8235 974,8476 1350,8597 L 5886,4061 C 5679,3826 5454,3602 5212,3389 Z M 9412,3696 L 9658,5937 10384,3696 9412,3696 Z M 5920,5680 L 5386,6631 7837,6825 5920,5680 Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5.12 2.8 15.98 18.27"> <path d="M12.101 2.90055C12.2146 2.83225 12.3508 2.8119 12.4794 2.84399L15.8755 3.69071C16.1324 3.75477 16.2929 4.00688 16.2467 4.26372C18.6108 5.37088 20.3713 7.19467 21.0609 9.24662C21.1343 9.46479 21.0492 9.70483 20.8548 9.82812C20.6605 9.95141 20.4071 9.9261 20.2409 9.7668C19.0305 8.60617 17.4469 7.59561 15.5992 6.86542L15.5744 6.96513C15.5423 7.0938 15.4604 7.20446 15.3467 7.27276C15.2331 7.34106 15.0969 7.3614 14.9683 7.32932L14.4831 7.20836L13.5759 10.847C13.8439 10.9138 14.0069 11.1851 13.9401 11.4531L11.6418 20.6709C11.6098 20.7996 11.5279 20.9102 11.4142 20.9785C11.3005 21.0468 11.1644 21.0672 11.0357 21.0351L8.60999 20.4303C8.34205 20.3635 8.179 20.0921 8.24581 19.8242L10.5441 10.6064C10.5761 10.4777 10.658 10.367 10.7717 10.2987C10.8854 10.2304 11.0215 10.2101 11.1502 10.2422L12.0574 6.60356L11.5722 6.48259C11.3043 6.41579 11.1412 6.14443 11.208 5.87649L11.2329 5.77678C9.25866 5.55406 7.38598 5.70288 5.77237 6.15943C5.5509 6.22209 5.31527 6.12547 5.20155 5.92537C5.08782 5.72527 5.12539 5.47339 5.29255 5.31518C6.86478 3.82713 9.27548 3.04332 11.8826 3.17562C11.9196 3.06091 11.997 2.96302 12.101 2.90055Z" fill="#b4b4b4"></path> </svg>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -45,8 +45,8 @@ $dropdown-link-hover-bg: #11131f;
|
||||
$dropdown-link-active-color: #fff;
|
||||
$dropdown-link-active-bg: #11131f;
|
||||
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import '~tlite/tlite.css';
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import 'tlite/tlite.css';
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
@@ -993,6 +993,10 @@ th {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-audit {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.scriptmessage {
|
||||
@@ -1160,3 +1164,10 @@ app-master-page, app-liquid-master-page, app-bisq-master-page {
|
||||
app-global-footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user