Merge branch 'master' into add-faq
This commit is contained in:
@@ -169,7 +169,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="community-integrations-sponsor">
|
||||
<div class="selfhosted-integrations-sponsor">
|
||||
<h3 i18n="about.self-hosted-integrations">Self-Hosted Integrations</h3>
|
||||
<div class="wrapper">
|
||||
<a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel">
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
.alliances,
|
||||
.enterprise-sponsor,
|
||||
.community-integrations-sponsor,
|
||||
.selfhosted-integrations-sponsor,
|
||||
.maintainers {
|
||||
margin-top: 68px;
|
||||
margin-bottom: 68px;
|
||||
@@ -108,6 +109,7 @@
|
||||
.contributors,
|
||||
.community-sponsor,
|
||||
.community-integrations-sponsor,
|
||||
.selfhosted-integrations-sponsor,
|
||||
.maintainers {
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
@@ -181,3 +183,8 @@
|
||||
.no-about-margin {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.community-integrations-sponsor {
|
||||
max-width: 750px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<span
|
||||
*ngIf="multisig"
|
||||
*ngIf="label"
|
||||
class="badge badge-pill badge-warning"
|
||||
i18n="address-labels.multisig"
|
||||
>multisig {{ multisigM }} of {{ multisigN }}</span>
|
||||
|
||||
<span
|
||||
*ngIf="lightning"
|
||||
class="badge badge-pill badge-warning"
|
||||
i18n="address-labels.upper-layer-peg-out"
|
||||
>Lightning {{ lightning }}</span>
|
||||
|
||||
<span
|
||||
*ngIf="liquid"
|
||||
class="badge badge-pill badge-warning"
|
||||
i18n="address-labels.upper-layer-peg-out"
|
||||
>Liquid {{ liquid }}</span>
|
||||
>{{ label }}</span>
|
||||
|
||||
@@ -14,12 +14,7 @@ export class AddressLabelsComponent implements OnInit {
|
||||
@Input() vin: Vin;
|
||||
@Input() vout: Vout;
|
||||
|
||||
multisig = false;
|
||||
multisigM: number;
|
||||
multisigN: number;
|
||||
|
||||
lightning = null;
|
||||
liquid = null;
|
||||
label?: string;
|
||||
|
||||
constructor(
|
||||
stateService: StateService,
|
||||
@@ -39,30 +34,46 @@ export class AddressLabelsComponent implements OnInit {
|
||||
if (this.vin.inner_witnessscript_asm) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0) {
|
||||
if (this.vin.witness.length > 11) {
|
||||
this.liquid = 'Peg Out';
|
||||
this.label = 'Liquid Peg Out';
|
||||
} else {
|
||||
this.liquid = 'Emergency Peg Out';
|
||||
this.label = 'Emergency Liquid Peg Out';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||
const topElement = this.vin.witness[this.vin.witness.length - 2];
|
||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSHBYTES_(1 \w{2}|2 \w{4}) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
if (this.vin.witness[this.vin.witness.length - 2] == '01') {
|
||||
this.lightning = 'Revoked Force Close';
|
||||
} else {
|
||||
this.lightning = 'Force Close';
|
||||
}
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||
} else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
if (this.vin.witness[this.vin.witness.length - 2].length == 66) {
|
||||
this.lightning = 'Revoked HTLC';
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||
if (topElement === '01') {
|
||||
// top element is '01' to get in the revocation path
|
||||
this.label = 'Revoked Lightning Force Close';
|
||||
} else {
|
||||
this.lightning = 'HTLC';
|
||||
// top element is '', this is a delayed to_local output
|
||||
this.label = 'Lightning Force Close';
|
||||
}
|
||||
return;
|
||||
} else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||
if (topElement.length === 66) {
|
||||
// top element is a public key
|
||||
this.label = 'Revoked Lightning HTLC';
|
||||
} else if (topElement) {
|
||||
// top element is a preimage
|
||||
this.label = 'Lightning HTLC';
|
||||
} else {
|
||||
// top element is '' to get in the multisig path of the script
|
||||
this.label = 'Expired Lightning HTLC';
|
||||
}
|
||||
return;
|
||||
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
||||
if (topElement) {
|
||||
// top element is a signature
|
||||
this.label = 'Lightning Anchor';
|
||||
} else {
|
||||
// top element is '', it has been swept after 16 blocks
|
||||
this.label = 'Swept Lightning Anchor';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lightning) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,19 +88,19 @@ export class AddressLabelsComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
const ops = script.split(' ');
|
||||
if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') {
|
||||
if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0]);
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||
if (ops.length < n * 2 + 1) {
|
||||
return;
|
||||
}
|
||||
// pop n public keys
|
||||
for (var i = 0; i < n; i++) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
|
||||
return;
|
||||
}
|
||||
@@ -101,13 +112,12 @@ export class AddressLabelsComponent implements OnInit {
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0]);
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
|
||||
this.multisig = true;
|
||||
this.multisigM = m;
|
||||
this.multisigN = n;
|
||||
this.label = `multisig ${m} of ${n}`;
|
||||
}
|
||||
|
||||
handleVout() {
|
||||
this.detectMultisig(this.vout.scriptpubkey_asm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
<h2>
|
||||
<h2 class="text-left">
|
||||
<ng-template [ngIf]="!transactions?.length"> </ng-template>
|
||||
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction</ng-template>
|
||||
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions</ng-template>
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
providers: [NgbTooltipConfig]
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
link: HTMLElement = document.getElementById('canonical');
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private location: Location,
|
||||
tooltipConfig: NgbTooltipConfig,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.dir = 'rtl';
|
||||
this.class = 'rtl-layout';
|
||||
}
|
||||
|
||||
tooltipConfig.animation = false;
|
||||
tooltipConfig.container = 'body';
|
||||
tooltipConfig.triggers = 'hover';
|
||||
}
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="full-container">
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<span i18n="mining.block-fees">Block fees</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1">
|
||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 3">
|
||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 7">
|
||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 30">
|
||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay > 1095">
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingStats>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.miners-luck">Hashrate</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,135 @@
|
||||
.card-header {
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 829px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 1130px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 1130px) {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
@media (min-width: 830px) {
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { formatterXAxisLabel } from 'src/app/shared/graphs.utils';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-fees-graph',
|
||||
templateUrl: './block-fees-graph.component.html',
|
||||
styleUrls: ['./block-fees-graph.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockFeesGraphComponent implements OnInit {
|
||||
@Input() tableOnly = false;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
statsObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
this.timespan = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalBlockFees$(timespan)
|
||||
.pipe(
|
||||
tap((data: any) => {
|
||||
this.prepareChartOptions({
|
||||
blockFees: data.blockFees.map(val => [val.timestamp * 1000, val.avg_fees / 100000000]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
|
||||
) / 3600 / 24;
|
||||
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
{ offset: 0.75, color: '#FDD835' },
|
||||
{ offset: 1, color: '#7CB342' }
|
||||
]),
|
||||
],
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 80,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks) => {
|
||||
const tick = ticks[0];
|
||||
const feesString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC`;
|
||||
return `
|
||||
<b style="color: white; margin-left: 18px">${tick.axisValueLabel}</b><br>
|
||||
<span>${feesString}</span>
|
||||
`;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
name: formatterXAxisLabel(this.locale, this.timespan),
|
||||
nameLocation: 'middle',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
type: 'time',
|
||||
splitNumber: this.isMobile() ? 5 : 10,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
return `${val} BTC`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Fees',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.blockFees,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
moveOnMouseMove: false,
|
||||
}, {
|
||||
showDetail: false,
|
||||
show: true,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="full-container">
|
||||
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<span i18n="mining.block-rewards">Block rewards</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1">
|
||||
<input ngbButton type="radio" [value]="'24h'" fragment="24h"> 24h
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 3">
|
||||
<input ngbButton type="radio" [value]="'3d'" fragment="3d"> 3D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 7">
|
||||
<input ngbButton type="radio" [value]="'1w'" fragment="1w"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 30">
|
||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay > 1095">
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingStats>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.miners-luck">Hashrate</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,135 @@
|
||||
.card-header {
|
||||
border-bottom: 0;
|
||||
font-size: 18px;
|
||||
@media (min-width: 465px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 829px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 1130px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 1130px) {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
@media (min-width: 830px) {
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { formatterXAxisLabel } from 'src/app/shared/graphs.utils';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-rewards-graph',
|
||||
templateUrl: './block-rewards-graph.component.html',
|
||||
styleUrls: ['./block-rewards-graph.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockRewardsGraphComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
statsObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private miningService: MiningService,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@mining.block-reward:Block Reward`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
this.timespan = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalBlockRewards$(timespan)
|
||||
.pipe(
|
||||
tap((data: any) => {
|
||||
this.prepareChartOptions({
|
||||
blockRewards: data.blockRewards.map(val => [val.timestamp * 1000, val.avg_rewards / 100000000]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
|
||||
) / 3600 / 24;
|
||||
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
{ offset: 0.75, color: '#FDD835' },
|
||||
{ offset: 1, color: '#7CB342' }
|
||||
]),
|
||||
],
|
||||
grid: {
|
||||
top: 20,
|
||||
bottom: 80,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks) => {
|
||||
const tick = ticks[0];
|
||||
const rewardsString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC`;
|
||||
return `
|
||||
<b style="color: white; margin-left: 18px">${tick.axisValueLabel}</b><br>
|
||||
<span>${rewardsString}</span>
|
||||
`;
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
name: formatterXAxisLabel(this.locale, this.timespan),
|
||||
nameLocation: 'middle',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
type: 'time',
|
||||
splitNumber: this.isMobile() ? 5 : 10,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
min: value => Math.round(10 * value.min * 0.99) / 10,
|
||||
max: value => Math.round(10 * value.max * 1.01) / 10,
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
return `${val} BTC`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Reward',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.blockRewards,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
moveOnMouseMove: false,
|
||||
}, {
|
||||
showDetail: false,
|
||||
show: true,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
}
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
<div #blockTxTitle id="block-tx-title" class="block-tx-title">
|
||||
<h2>
|
||||
<h2 class="text-left">
|
||||
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
|
||||
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
this.showMiningInfo = url === '/mining';
|
||||
this.showMiningInfo = url.indexOf('/mining') !== -1;
|
||||
this.cd.markForCheck(); // Need to update the view asap
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container-xl" [class]="widget ? 'widget' : ''">
|
||||
<div class="container-xl" [class]="widget ? 'widget' : 'full-height'">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@@ -25,8 +25,8 @@
|
||||
</td>
|
||||
<td class="pool text-left" [class]="widget ? 'widget' : ''">
|
||||
<div class="tooltip-custom">
|
||||
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
|
||||
<img width="25" height="25" src="{{ block.extras.pool['logo'] }}"
|
||||
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
|
||||
<img width="23" height="23" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = './resources/mining-pools/default.svg'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
</a>
|
||||
@@ -64,7 +64,7 @@
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="pool text-left" [class]="widget ? 'widget' : ''">
|
||||
<img width="0" height="25" style="opacity: 0">
|
||||
<img width="25" height="25" style="opacity: 0">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget">
|
||||
@@ -91,9 +91,9 @@
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="blocksCount" [rotate]="true" [maxSize]="5" [pageSize]="15" [(page)]="page"
|
||||
[collectionSize]="blocksCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
td {
|
||||
padding-top: 0.7rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
@media (max-width: 376px) {
|
||||
padding-top: 0.73rem !important;
|
||||
padding-bottom: 0.73rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
@@ -35,8 +39,7 @@ td {
|
||||
.pool.widget {
|
||||
width: 40%;
|
||||
padding-left: 30px;
|
||||
@media (max-width: 576px) {
|
||||
padding-left: 40px;
|
||||
@media (max-width: 376px) {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class BlocksList implements OnInit {
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
lastPage = 1;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
blocksCount: number;
|
||||
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromBlockHeight);
|
||||
skeletonLines: number[] = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="min-height: 295px">
|
||||
<table class="table latest-transactions">
|
||||
<table class="table latest-adjustments">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="d-none d-md-block" i18n="block.height">Height</th>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.latest-transactions {
|
||||
.latest-adjustments {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
@@ -7,34 +7,8 @@
|
||||
}
|
||||
td {
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-satoshis {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: table-cell;
|
||||
@media (max-width: 376px) {
|
||||
padding: 0.85rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fiat {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fees {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, HostBinding } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Env, StateService } from 'src/app/services/state.service';
|
||||
|
||||
@@ -14,6 +14,8 @@ export class DocsComponent implements OnInit {
|
||||
showWebSocketTab = true;
|
||||
showFaqTab = true;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
@@ -32,6 +34,7 @@ export class DocsComponent implements OnInit {
|
||||
this.env = this.stateService.env;
|
||||
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
|
||||
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
|
||||
|
||||
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
<ul ngbNav #nav="ngbNav" class="nav-pills mb-3" style="padding: 0px 35px" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<div class="d-inline-flex flex-wrap menu">
|
||||
<li ngbNavItem class="menu-li">
|
||||
<a routerLinkActive="active" [routerLink]="['/graphs/mempool' | relativeUrl]" ngbNavLink>Mempool</a>
|
||||
</li>
|
||||
<li ngbNavItem class="menu-li">
|
||||
<a routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]" ngbNavLink i18n="mining.pools">
|
||||
<div class="mb-3 d-flex menu" style="padding: 0px 35px;">
|
||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1"
|
||||
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
|
||||
<div ngbDropdown *ngIf="stateService.env.MINING_DASHBOARD" class="w-50">
|
||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
|
||||
i18n="mining.pools">
|
||||
Pools ranking
|
||||
</a>
|
||||
</li>
|
||||
<li ngbNavItem class="menu-li">
|
||||
<a routerLinkActive="active" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" ngbNavLink i18n="mining.pools-dominance">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"
|
||||
i18n="mining.pools-dominance">
|
||||
Pools dominance
|
||||
</a>
|
||||
</li>
|
||||
<li ngbNavItem class="menu-li">
|
||||
<a routerLinkActive="active" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" ngbNavLink
|
||||
i18n="mining.hashrate-difficulty">
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">
|
||||
Hashrate & Difficulty
|
||||
</a>
|
||||
</li>
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">
|
||||
Block Fees
|
||||
</a>
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">
|
||||
Block Rewards
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.menu {
|
||||
flex-grow: 1;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.menu-li {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
@media (min-width: 576px) {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { StateService } from "src/app/services/state.service";
|
||||
import { WebsocketService } from "src/app/services/websocket.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-graphs',
|
||||
@@ -7,9 +8,12 @@ import { StateService } from "src/app/services/state.service";
|
||||
styleUrls: ['./graphs.component.scss'],
|
||||
})
|
||||
export class GraphsComponent implements OnInit {
|
||||
constructor(public stateService: StateService) { }
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,37 +19,40 @@
|
||||
</div>
|
||||
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<span i18n="mining.mining-pool-share">Hashrate & Difficulty</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||
<span i18n="mining.hashrate-difficulty">Hashrate & Difficulty</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'"> 3M
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 30">
|
||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'"> 6M
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'"> 2Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'"> 3Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'"> ALL
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay > 1095">
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingStats>
|
||||
@@ -67,4 +70,4 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100% - 170px);
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
height: calc(100% - 220px);
|
||||
};
|
||||
@media (max-width: 575px) {
|
||||
height: calc(100% - 260px);
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,17 +92,8 @@
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
margin: 0px auto 10px;
|
||||
display: inline-block;
|
||||
@media (min-width: 485px) {
|
||||
margin: 0px auto 10px;
|
||||
}
|
||||
@media (min-width: 785px) {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
@@ -142,4 +132,4 @@
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
@@ -7,6 +7,8 @@ import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart',
|
||||
@@ -28,15 +30,16 @@ export class HashrateChartComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
hashrateObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
@@ -47,20 +50,32 @@ export class HashrateChartComponent implements OnInit {
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
let firstRun = true;
|
||||
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith('1y'),
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalHashrate$(timespan)
|
||||
.pipe(
|
||||
@@ -157,10 +172,10 @@ export class HashrateChartComponent implements OnInit {
|
||||
'#D81B60',
|
||||
],
|
||||
grid: {
|
||||
top: 30,
|
||||
top: 20,
|
||||
bottom: this.widget ? 30 : 70,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: this.widget ? 30 : this.isMobile() ? 90 : 60,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile() || !this.widget,
|
||||
@@ -176,7 +191,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (ticks) {
|
||||
formatter: (ticks) => {
|
||||
let hashrateString = '';
|
||||
let difficultyString = '';
|
||||
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
||||
@@ -207,11 +222,14 @@ export class HashrateChartComponent implements OnInit {
|
||||
<span>${hashrateString}</span><br>
|
||||
<span>${difficultyString}</span>
|
||||
`;
|
||||
}.bind(this)
|
||||
}
|
||||
},
|
||||
xAxis: data.hashrates.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
},
|
||||
legend: (this.widget || data.hashrates.length === 0) ? undefined : {
|
||||
data: [
|
||||
@@ -241,7 +259,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
},
|
||||
yAxis: data.hashrates.length === 0 ? undefined : [
|
||||
{
|
||||
min: function (value) {
|
||||
min: (value) => {
|
||||
return value.min * 0.9;
|
||||
},
|
||||
type: 'value',
|
||||
@@ -250,7 +268,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
formatter: (val) => {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
@@ -258,7 +276,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
},
|
||||
{
|
||||
min: function (value) {
|
||||
min: (value) => {
|
||||
return value.min * 0.9;
|
||||
},
|
||||
type: 'value',
|
||||
@@ -268,7 +286,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
formatter: (val) => {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}`
|
||||
return `${newVal} ${selectedPowerOfTen.unit}`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
@@ -278,6 +296,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
],
|
||||
series: data.hashrates.length === 0 ? [] : [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Hashrate',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
@@ -288,6 +307,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 1,
|
||||
name: 'Difficulty',
|
||||
showSymbol: false,
|
||||
@@ -312,7 +332,6 @@ export class HashrateChartComponent implements OnInit {
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
bottom: this.isMobile() ? 30 : 0,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<div [class]="widget === false ? 'full-container' : ''">
|
||||
<div class="full-container">
|
||||
|
||||
<div class="card-header" [style]="widget ? 'display:none' : ''">
|
||||
<span *ngIf="!widget" i18n="mining.pools-dominance">Mining pools dominance</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||
<div class="card-header mb-0 mb-md-4">
|
||||
<span i18n="mining.pools-dominance">Mining pools dominance</span>
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'"> 3M
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 30">
|
||||
<input ngbButton type="radio" [value]="'1m'" fragment="1m"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'"> 6M
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'"> 2Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'"> 3Y
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'"> ALL
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay > 1095">
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
<div class="chart"
|
||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
||||
@@ -20,19 +20,18 @@
|
||||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100% - 140px);
|
||||
@media (max-width: 991px) {
|
||||
height: calc(100% - 190px);
|
||||
};
|
||||
@media (max-width: 575px) {
|
||||
height: calc(100% - 235px);
|
||||
min-height: 500px;
|
||||
height: calc(100% - 150px);
|
||||
@media (max-width: 992px) {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
@@ -43,12 +42,6 @@
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 85px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 85px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
@@ -6,6 +6,8 @@ import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { poolsColor } from 'src/app/app.constants';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart-pools',
|
||||
@@ -22,19 +24,19 @@ import { poolsColor } from 'src/app/app.constants';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HashrateChartPoolsComponent implements OnInit {
|
||||
@Input() widget = false;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 25;
|
||||
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
hashrateObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
|
||||
@@ -44,20 +46,29 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`);
|
||||
}
|
||||
let firstRun = true;
|
||||
|
||||
this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith('1y'),
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
if (!firstRun) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.isLoading = true;
|
||||
return this.apiService.getHistoricalPoolsHashrate$(timespan)
|
||||
.pipe(
|
||||
@@ -75,6 +86,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
const legends = [];
|
||||
for (const name in grouped) {
|
||||
series.push({
|
||||
zlevel: 0,
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
@@ -84,7 +96,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
lineStyle: { width: 0 },
|
||||
areaStyle: { opacity: 1 },
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
@@ -157,11 +169,11 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: this.widget ? 30 : 60,
|
||||
top: this.widget || this.isMobile() ? 10 : 50,
|
||||
bottom: 70,
|
||||
top: this.isMobile() ? 10 : 50,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile() || !this.widget,
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
@@ -188,9 +200,12 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
},
|
||||
xAxis: data.series.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
splitNumber: this.isMobile() ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
},
|
||||
legend: (this.isMobile() || this.widget || data.series.length === 0) ? undefined : {
|
||||
legend: (this.isMobile() || data.series.length === 0) ? undefined : {
|
||||
data: data.legends
|
||||
},
|
||||
yAxis: data.series.length === 0 ? undefined : {
|
||||
@@ -207,7 +222,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
min: 0,
|
||||
},
|
||||
series: data.series,
|
||||
dataZoom: this.widget ? null : [{
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
@@ -220,7 +235,6 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
bottom: 0,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
|
||||
@@ -157,6 +157,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
data: this.data.series[0],
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
|
||||
@@ -62,7 +62,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
this.showMiningInfo = url === '/mining';
|
||||
this.showMiningInfo = url.indexOf('/mining') !== -1;
|
||||
this.cd.markForCheck(); // Need to update the view asap
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
if (index >= this.feeLimitIndex) {
|
||||
newColors.push(this.chartColorsOrdered[index]);
|
||||
seriesGraph.push({
|
||||
zlevel: 0,
|
||||
name: this.feeLevelsOrdered[index],
|
||||
type: 'line',
|
||||
stack: 'fees',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<ng-template #done>
|
||||
<ng-template [ngIf]="miner" [ngIfElse]="unknownMiner">
|
||||
<a placement="bottom" [ngbTooltip]="title" [href]="url" target="_blank" class="badge badge-primary">{{ miner }}</a>
|
||||
<a placement="bottom" [ngbTooltip]="title" [href]="url" [target]="target" class="badge badge-primary">{{ miner }}</a>
|
||||
</ng-template>
|
||||
<ng-template #unknownMiner>
|
||||
<span class="badge badge-secondary" i18n="miner.tag.unknown-miner">Unknown</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { Transaction } from 'src/app/interfaces/electrs.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-miner',
|
||||
@@ -13,15 +15,23 @@ export class MinerComponent implements OnChanges {
|
||||
miner = '';
|
||||
title = '';
|
||||
url = '';
|
||||
target = '_blank';
|
||||
loading = true;
|
||||
|
||||
constructor(
|
||||
private assetsService: AssetsService,
|
||||
private cd: ChangeDetectorRef,
|
||||
public stateService: StateService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
) { }
|
||||
|
||||
ngOnChanges() {
|
||||
this.miner = '';
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
this.miner = 'Unknown';
|
||||
this.url = this.relativeUrlPipe.transform(`/mining/pool/unknown`);
|
||||
this.target = '';
|
||||
}
|
||||
this.loading = true;
|
||||
this.findMinerFromCoinbase();
|
||||
}
|
||||
@@ -40,7 +50,13 @@ export class MinerComponent implements OnChanges {
|
||||
if (pools.payout_addresses[vout.scriptpubkey_address]) {
|
||||
this.miner = pools.payout_addresses[vout.scriptpubkey_address].name;
|
||||
this.title = $localize`:@@miner-identified-by-payout:Identified by payout address: '${vout.scriptpubkey_address}:PAYOUT_ADDRESS:'`;
|
||||
this.url = pools.payout_addresses[vout.scriptpubkey_address].link;
|
||||
const pool = pools.payout_addresses[vout.scriptpubkey_address];
|
||||
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
|
||||
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
|
||||
this.target = '';
|
||||
} else {
|
||||
this.url = pool.link;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -48,9 +64,15 @@ export class MinerComponent implements OnChanges {
|
||||
if (pools.coinbase_tags.hasOwnProperty(tag)) {
|
||||
const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig);
|
||||
if (coinbaseAscii.indexOf(tag) > -1) {
|
||||
this.miner = pools.coinbase_tags[tag].name;
|
||||
const pool = pools.coinbase_tags[tag];
|
||||
this.miner = pool.name;
|
||||
this.title = $localize`:@@miner-identified-by-coinbase:Identified by coinbase tag: '${tag}:TAG:'`;
|
||||
this.url = pools.coinbase_tags[tag].link;
|
||||
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
|
||||
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
|
||||
this.target = '';
|
||||
} else {
|
||||
this.url = pool.link;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<!-- pool distribution -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 385px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-pool-ranking [widget]=true></app-pool-ranking>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<!-- hashrate -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 385px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-hashrate-chart [widget]=true></app-hashrate-chart>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<!-- Latest blocks -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 385px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Latest blocks
|
||||
@@ -63,13 +63,13 @@
|
||||
|
||||
<!-- Difficult adjustments -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 385px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Adjustments
|
||||
</h5>
|
||||
<app-difficulty-adjustments-table></app-difficulty-adjustments-table>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
<div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
»</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay > 1095">
|
||||
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
|
||||
</label>
|
||||
</div>
|
||||
@@ -63,8 +63,12 @@
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'bottom-padding' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)"></div>
|
||||
<div>
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
@@ -75,7 +79,7 @@
|
||||
<th class="d-none d-md-block" i18n="mining.rank">Rank</th>
|
||||
<th class=""></th>
|
||||
<th class="" i18n="mining.pool-name">Pool</th>
|
||||
<th class="" *ngIf="this.poolsWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
|
||||
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
|
||||
<th class="" i18n="master-page.blocks">Blocks</th>
|
||||
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty Blocks</th>
|
||||
</tr>
|
||||
@@ -85,8 +89,8 @@
|
||||
<td class="d-none d-md-block">{{ pool.rank }}</td>
|
||||
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}"
|
||||
onError="this.src = './resources/mining-pools/default.svg'"></td>
|
||||
<td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="this.poolsWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
|
||||
<td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</td>
|
||||
<td class="">{{ pool['blockText'] }}</td>
|
||||
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
|
||||
@@ -95,7 +99,7 @@
|
||||
<td class="d-none d-md-block"></td>
|
||||
<td class="text-right"></td>
|
||||
<td class="" i18n="mining.all-miners"><b>All miners</b></td>
|
||||
<td class="" *ngIf="this.poolsWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||
<td class="d-none d-md-block"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||
@@ -129,4 +133,4 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -28,7 +28,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 485px) {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
@@ -93,17 +93,8 @@
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
margin: 0px auto 10px;
|
||||
display: inline-block;
|
||||
@media (min-width: 485px) {
|
||||
margin: 0px auto 10px;
|
||||
}
|
||||
@media (min-width: 785px) {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
@@ -10,6 +10,7 @@ import { StorageService } from '../..//services/storage.service';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { chartColors, poolsColor } from 'src/app/app.constants';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pool-ranking',
|
||||
@@ -18,20 +19,20 @@ import { chartColors, poolsColor } from 'src/app/app.constants';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PoolRankingComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() widget = false;
|
||||
|
||||
poolsWindowPreference: string;
|
||||
miningWindowPreference: string;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
isLoading = true;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
chartInstance: any = undefined;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
miningStatsObservable$: Observable<MiningStats>;
|
||||
|
||||
constructor(
|
||||
@@ -47,13 +48,13 @@ export class PoolRankingComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.widget) {
|
||||
this.poolsWindowPreference = '1w';
|
||||
this.miningWindowPreference = '1w';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
|
||||
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
// When...
|
||||
this.miningStatsObservable$ = combineLatest([
|
||||
@@ -66,12 +67,12 @@ export class PoolRankingComponent implements OnInit {
|
||||
// ...or we change the timespan
|
||||
this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.poolsWindowPreference), // (trigger when the page loads)
|
||||
startWith(this.miningWindowPreference), // (trigger when the page loads)
|
||||
tap((value) => {
|
||||
if (!this.widget) {
|
||||
this.storageService.setValue('poolsWindowPreference', value);
|
||||
this.storageService.setValue('miningWindowPreference', value);
|
||||
}
|
||||
this.poolsWindowPreference = value;
|
||||
this.miningWindowPreference = value;
|
||||
})
|
||||
)
|
||||
])
|
||||
@@ -79,7 +80,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
this.isLoading = true;
|
||||
return this.miningService.getMiningStats(this.poolsWindowPreference)
|
||||
return this.miningService.getMiningStats(this.miningWindowPreference)
|
||||
.pipe(
|
||||
catchError((e) => of(this.getEmptyMiningStat()))
|
||||
);
|
||||
@@ -149,7 +150,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
if (this.poolsWindowPreference === '24h') {
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
||||
`<br>` + pool.blockCount.toString() + ` blocks`;
|
||||
@@ -159,7 +160,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
},
|
||||
data: pool.poolId,
|
||||
data: pool.slug,
|
||||
} as PieSeriesOption);
|
||||
});
|
||||
|
||||
@@ -185,7 +186,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
if (this.poolsWindowPreference === '24h') {
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||
totalEstimatedHashrateOther.toString() + ' PH/s' +
|
||||
`<br>` + totalBlockOther.toString() + ` blocks`;
|
||||
@@ -202,30 +203,6 @@ export class PoolRankingComponent implements OnInit {
|
||||
}
|
||||
|
||||
prepareChartOptions(miningStats) {
|
||||
let network = this.stateService.network;
|
||||
if (network === '') {
|
||||
network = 'bitcoin';
|
||||
}
|
||||
network = network.charAt(0).toUpperCase() + network.slice(1);
|
||||
|
||||
let radius: any[] = ['20%', '80%'];
|
||||
let top: number = 0; let height = undefined;
|
||||
if (this.isMobile() && this.widget) {
|
||||
top = -30;
|
||||
height = 270;
|
||||
radius = ['10%', '50%'];
|
||||
} else if (this.isMobile() && !this.widget) {
|
||||
top = -40;
|
||||
height = 300;
|
||||
radius = ['10%', '50%'];
|
||||
} else if (this.widget) {
|
||||
radius = ['15%', '60%'];
|
||||
top = -20;
|
||||
height = 330;
|
||||
} else {
|
||||
top = 0;
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
color: chartColors,
|
||||
@@ -237,12 +214,11 @@ export class PoolRankingComponent implements OnInit {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
minShowLabelAngle: 3.6,
|
||||
top: top,
|
||||
height: height,
|
||||
name: 'Mining pool',
|
||||
type: 'pie',
|
||||
radius: radius,
|
||||
radius: ['20%', '80%'],
|
||||
data: this.generatePoolsChartSerieData(miningStats),
|
||||
labelLine: {
|
||||
lineStyle: {
|
||||
@@ -284,7 +260,8 @@ export class PoolRankingComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
this.router.navigate(['/mining/pool/', e.data.data]);
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/mining/pool/${e.data.data}`);
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<!-- Pool overview -->
|
||||
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
|
||||
<div style="display:flex" class="mb-3">
|
||||
<img width="50" height="50" src="{{ poolStats['logo'] }}"
|
||||
@@ -9,107 +10,268 @@
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<table class="table table-borderless table-striped" style="table-layout: fixed;">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<table class="table table-borderless table-striped taller" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Tags</td>
|
||||
<td class="text-truncate" *ngIf="poolStats.pool.regexes.length else nodata">
|
||||
<div class="scrollable">
|
||||
|
||||
<!-- Regexes desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.tags">Tags</td>
|
||||
<td *ngIf="poolStats.pool.regexes.length else nodata" style="vertical-align: middle">
|
||||
<div class="scrollable">{{ poolStats.pool.regexes }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Regexes mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.tags">Tags</span>
|
||||
<div *ngIf="poolStats.pool.regexes.length else nodatamobile" class="overflow-auto">
|
||||
{{ poolStats.pool.regexes }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Addresses</td>
|
||||
<td class="text-truncate" *ngIf="poolStats.pool.addresses.length else nodata">
|
||||
<div class="scrollable">
|
||||
<a *ngFor="let address of poolStats.pool.addresses"
|
||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||
address }}<br></a>
|
||||
|
||||
<!-- Addresses desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label addresses" i18n="mining.addresses">Addresses</td>
|
||||
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px">
|
||||
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address">
|
||||
{{ poolStats.pool.addresses[0] }}
|
||||
</a>
|
||||
<div>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
|
||||
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||
address }}<br></a>
|
||||
</div>
|
||||
<button *ngIf="poolStats.pool.addresses.length >= 2" type="button"
|
||||
class="btn btn-sm btn-primary small-button" (click)="collapse.toggle()"
|
||||
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
|
||||
<div *ngIf="gfg"><span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
|
||||
</div>
|
||||
<span *ngIf="!gfg" i18n="hide">Hide</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Addresses mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.addresses">Addresses</span>
|
||||
<div *ngIf="poolStats.pool.addresses.length else nodatamobile">
|
||||
<button *ngIf="poolStats.pool.addresses.length >= 2" type="button"
|
||||
class="btn btn-sm btn-primary float-right small-button mobile" (click)="collapse.toggle()"
|
||||
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
|
||||
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
|
||||
</button>
|
||||
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
||||
{{ poolStats.pool.addresses[0] | shortenString: 40 }}
|
||||
</a>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
|
||||
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||
address | shortenString: 40 }}<br></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<ng-template #nodata>
|
||||
<td class="right-mobile">~</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Mined Blocks</td>
|
||||
<td class="data">{{ formatNumber(poolStats.blockCount, this.locale, '1.0-0') }}</td>
|
||||
|
||||
<!-- Hashrate desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
|
||||
<td class="data">
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||
<ng-template *ngIf="poolStats.luck; else noreported">
|
||||
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Empty Blocks</td>
|
||||
<td class="data">{{ formatNumber(poolStats.emptyBlocks, this.locale, '1.0-0') }}</td>
|
||||
<!-- Hashrate mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan="2">
|
||||
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||
<ng-template *ngIf="poolStats.luck; else noreported">
|
||||
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<ng-template #noreported>
|
||||
<td>~</td>
|
||||
<td>~</td>
|
||||
</ng-template>
|
||||
|
||||
<!-- Mined blocks desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
|
||||
<td class="data">
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Mined blocks mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
|
||||
<table class="table table-xs table-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #nodata>
|
||||
<td class="taller-row" style="vertical-align: middle">~</td>
|
||||
</ng-template>
|
||||
<ng-template #nodatamobile>
|
||||
<div>~</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- Hashrate chart -->
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll
|
||||
[infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50"
|
||||
(scrolled)="loadMore()">
|
||||
<thead>
|
||||
<th class="height" i18n="latest-blocks.height">Height</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined">Mined</th>
|
||||
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
|
||||
Coinbase Tag</th>
|
||||
<th class="reward text-right" i18n="latest-blocks.reward">
|
||||
Reward</th>
|
||||
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
||||
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
|
||||
<th class="size" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td class="height">
|
||||
<a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
|
||||
}}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td class="mined">
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td class="coinbase">
|
||||
<span class="badge badge-secondary scriptmessage longer">
|
||||
{{ block.extras.coinbaseRaw | hex2ascii }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="reward text-right">
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td class="fees text-right">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td class="txs text-right">
|
||||
{{ block.tx_count | number }}
|
||||
</td>
|
||||
<td class="size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" 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>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Blocks list -->
|
||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
|
||||
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">
|
||||
<thead *ngIf="blocks.length > 0">
|
||||
<th class="height" i18n="latest-blocks.height">Height</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined">Mined</th>
|
||||
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
|
||||
Coinbase Tag</th>
|
||||
<th class="reward text-right" i18n="latest-blocks.reward">
|
||||
Reward</th>
|
||||
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
||||
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
|
||||
<th class="size" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td class="height">
|
||||
<a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
|
||||
}}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td class="mined">
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td class="coinbase">
|
||||
<span class="badge badge-secondary scriptmessage longer">
|
||||
{{ block.extras.coinbaseRaw | hex2ascii }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="reward text-right">
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td class="fees text-right">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td class="txs text-right">
|
||||
{{ block.tx_count | number }}
|
||||
</td>
|
||||
<td class="size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" 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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #skeleton>
|
||||
<thead>
|
||||
<th class="height" i18n="latest-blocks.height">Height</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined">Mined</th>
|
||||
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
|
||||
Coinbase Tag</th>
|
||||
<th class="reward text-right" i18n="latest-blocks.reward">
|
||||
Reward</th>
|
||||
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
||||
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
|
||||
<th class="size" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of [1,2,3,4,5]">
|
||||
<td class="height">
|
||||
@@ -143,56 +305,184 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Main table skeleton -->
|
||||
<ng-template #loadingMain>
|
||||
<div>
|
||||
<div class="mb-3" style="display:flex; position: relative">
|
||||
<div class="skeleton-loader mr-3" style="width: 50px; height: 50px"></div>
|
||||
<h1 class="m-0 pt-1 pt-md-0"><div class="skeleton-loader" style="position: absolute; top: 32%; width: 150px; height: 20px"></div></h1>
|
||||
<h1 class="m-0 pt-1 pt-md-0">
|
||||
<div class="skeleton-loader" style="position: absolute; top: 32%; width: 150px; height: 20px"></div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<table class="table table-borderless table-striped">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<table class="table table-borderless table-striped taller" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Tags</td>
|
||||
<td class="text-truncate">
|
||||
|
||||
<!-- Regexes desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.tags">Tags</td>
|
||||
<td style="vertical-align: middle">
|
||||
<div class="skeleton-loader"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Addresses</td>
|
||||
<td class="text-truncate">
|
||||
<div class="scrollable">
|
||||
<!-- Regexes mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.tags">Tags</span>
|
||||
<div class="overflow-auto">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</td>
|
||||
<ng-template #nodata>
|
||||
<td>~</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
|
||||
<!-- Addresses desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.addresses">Addresses</td>
|
||||
<td style="vertical-align: middle;">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Addresses mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.addresses">Addresses</span>
|
||||
<div>
|
||||
<div class="skeleton-loader"></div>
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<table class="table table-borderless table-striped" >
|
||||
|
||||
<div class="col-lg-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Mined Blocks</td>
|
||||
<td class="text-truncate">
|
||||
<div class="skeleton-loader"></div>
|
||||
|
||||
<!-- Hashrate desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
|
||||
<td class="data">
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Empty Blocks</td>
|
||||
<td class="text-truncate">
|
||||
<div class="skeleton-loader"></div>
|
||||
<!-- Hashrate mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan="2">
|
||||
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
|
||||
</th>
|
||||
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Mined blocks desktop -->
|
||||
<tr *ngIf="!isMobile()" class="taller-row">
|
||||
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
|
||||
<td class="data">
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Mined blocks mobile -->
|
||||
<tr *ngIf="isMobile()">
|
||||
<td colspan=2>
|
||||
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
|
||||
<table class="table table-xs table-data text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
|
||||
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="skeleton-loader data"></div>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-bottom: 20px;
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
div.scrollable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -42,16 +50,30 @@ div.scrollable {
|
||||
|
||||
.box {
|
||||
padding-bottom: 5px;
|
||||
@media (min-width: 767.98px) {
|
||||
min-height: 187px;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 35%;
|
||||
width: 25%;
|
||||
@media (min-width: 767.98px) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.label.addresses {
|
||||
vertical-align: top;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: left;
|
||||
padding-left: 25%;
|
||||
@media (max-width: 991px) {
|
||||
text-align: right;
|
||||
padding-left: 5%;
|
||||
@media (max-width: 992px) {
|
||||
text-align: left;
|
||||
padding-left: 12px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
@@ -103,10 +125,6 @@ div.scrollable {
|
||||
}
|
||||
}
|
||||
|
||||
.fees {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.size {
|
||||
width: 12%;
|
||||
@media (max-width: 1000px) {
|
||||
@@ -132,15 +150,13 @@ div.scrollable {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.right-mobile {
|
||||
@media (max-width: 450px) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 200px;
|
||||
}
|
||||
.skeleton-loader.data {
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
@@ -151,3 +167,41 @@ div.scrollable {
|
||||
top: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.small-button {
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.small-button.mobile {
|
||||
transform: translateY(-20px);
|
||||
@media (min-width: 767.98px) {
|
||||
transform: translateY(-17px);
|
||||
}
|
||||
}
|
||||
|
||||
.block-count-title {
|
||||
color: #4a68b9;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
@media (max-width: 767.98px) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.table-data tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
.table-data td {
|
||||
text-align: left;
|
||||
@media (max-width: 767.98px) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.taller-row {
|
||||
height: 75px;
|
||||
}
|
||||
@@ -2,12 +2,13 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit }
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { BehaviorSubject, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pool',
|
||||
@@ -19,6 +20,8 @@ export class PoolComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
gfg = true;
|
||||
|
||||
formatNumber = formatNumber;
|
||||
poolStats$: Observable<PoolStat>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
@@ -27,43 +30,45 @@ export class PoolComponent implements OnInit {
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
|
||||
blocks: BlockExtended[] = [];
|
||||
poolId: number = undefined;
|
||||
slug: string = undefined;
|
||||
|
||||
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.poolId);
|
||||
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.poolStats$ = this.route.params.pipe(map((params) => params.poolId))
|
||||
this.poolStats$ = this.route.params.pipe(map((params) => params.slug))
|
||||
.pipe(
|
||||
switchMap((poolId: any) => {
|
||||
switchMap((slug: any) => {
|
||||
this.isLoading = true;
|
||||
this.poolId = poolId;
|
||||
this.loadMoreSubject.next(this.poolId);
|
||||
return this.apiService.getPoolHashrate$(this.poolId)
|
||||
this.slug = slug;
|
||||
return this.apiService.getPoolHashrate$(this.slug)
|
||||
.pipe(
|
||||
switchMap((data) => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]));
|
||||
return poolId;
|
||||
return [slug];
|
||||
}),
|
||||
);
|
||||
}),
|
||||
switchMap(() => {
|
||||
return this.apiService.getPoolStats$(this.poolId);
|
||||
switchMap((slug) => {
|
||||
return this.apiService.getPoolStats$(slug);
|
||||
}),
|
||||
tap(() => {
|
||||
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
||||
}),
|
||||
map((poolStats) => {
|
||||
this.seoService.setTitle(poolStats.pool.name);
|
||||
let regexes = '"';
|
||||
for (const regex of poolStats.pool.regexes) {
|
||||
regexes += regex + '", "';
|
||||
@@ -71,6 +76,10 @@ export class PoolComponent implements OnInit {
|
||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||
poolStats.pool.addresses = poolStats.pool.addresses;
|
||||
|
||||
if (poolStats.reportedHashrate) {
|
||||
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
logo: `./resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||
}, poolStats);
|
||||
@@ -79,21 +88,37 @@ export class PoolComponent implements OnInit {
|
||||
|
||||
this.blocks$ = this.loadMoreSubject
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((flag) => {
|
||||
if (this.poolId === undefined) {
|
||||
if (this.slug === undefined) {
|
||||
return [];
|
||||
}
|
||||
return this.apiService.getPoolBlocks$(this.poolId, this.blocks[this.blocks.length - 1]?.height);
|
||||
return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height);
|
||||
}),
|
||||
tap((newBlocks) => {
|
||||
this.blocks = this.blocks.concat(newBlocks);
|
||||
}),
|
||||
map(() => this.blocks)
|
||||
map(() => this.blocks),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: `No data`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
@@ -124,7 +149,7 @@ export class PoolComponent implements OnInit {
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function(ticks: any[]) {
|
||||
formatter: function (ticks: any[]) {
|
||||
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
||||
let hashrate = ticks[0].data[1];
|
||||
|
||||
@@ -142,6 +167,9 @@ export class PoolComponent implements OnInit {
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile()) ? 5 : 10,
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
@@ -164,6 +192,7 @@ export class PoolComponent implements OnInit {
|
||||
],
|
||||
series: [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Hashrate',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
@@ -174,6 +203,34 @@ export class PoolComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
],
|
||||
dataZoom: data.length === 0 ? undefined : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
zoomLock: true,
|
||||
maxSpan: 100,
|
||||
minSpan: 10,
|
||||
moveOnMouseMove: false,
|
||||
}, {
|
||||
fillerColor: '#aaaaff15',
|
||||
borderColor: '#ffffff88',
|
||||
showDetail: false,
|
||||
show: true,
|
||||
type: 'slider',
|
||||
brushSelect: false,
|
||||
realtime: true,
|
||||
bottom: 0,
|
||||
left: 20,
|
||||
right: 15,
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#fff',
|
||||
opacity: 0.45,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,7 +239,7 @@ export class PoolComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.loadMoreSubject.next(this.poolId);
|
||||
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div class="text-left">
|
||||
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bisq.markets/">bisq.markets</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://wq.apnic.net/static/search.html?query=AS142052">AS142052</a>.</p>
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bisq.markets/">bisq.markets</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip"
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc"
|
||||
ngbTooltip="Amount being paid to miners in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">
|
||||
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
@@ -14,10 +14,10 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip"
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc"
|
||||
ngbTooltip="Average miners' reward per transaction in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">
|
||||
{{ rewardStats.rewardPerTx | amountShortener }}
|
||||
{{ rewardStats.rewardPerTx | amountShortener: 2 }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
@@ -27,9 +27,9 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip"
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
||||
ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">{{ rewardStats.feePerTx | amountShortener }}
|
||||
<div class="fee-text">{{ rewardStats.feePerTx | amountShortener: 2 }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
@@ -65,55 +65,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- <div class="reward-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
|
||||
<div class="card-text">
|
||||
<app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
<div class="symbol" i18n="rewardStats.totalReward-desc">were rewarded to miners</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
|
||||
<div class="card-text">
|
||||
{{ rewardStats.rewardPerTx | amountShortener }}
|
||||
<span class="symbol" i18n="mining.sats-per-tx">sats/tx</span>
|
||||
<div class="symbol" i18n="mining.rewards-per-tx-desc">miners reward / tx count</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
|
||||
<div class="card-text">
|
||||
{{ rewardStats.feePerTx | amountShortener}}
|
||||
<span class="symbol">sats/tx</span>
|
||||
<div class="symbol" i18n="mining.average-fee-desc">were paid per tx</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingReward>
|
||||
<div class="reward-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards">Miners Reward</h5>
|
||||
<div class="card-text skeleton">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Reward Per Tx</h5>
|
||||
<div class="card-text skeleton">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
|
||||
<div class="card-text skeleton">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template> -->
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, skip, switchMap } from 'rxjs/operators';
|
||||
import { concat, Observable } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@@ -12,25 +12,39 @@ import { StateService } from 'src/app/services/state.service';
|
||||
})
|
||||
export class RewardStatsComponent implements OnInit {
|
||||
public $rewardStats: Observable<any>;
|
||||
private lastBlockHeight: number;
|
||||
|
||||
constructor(private apiService: ApiService, private stateService: StateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.$rewardStats = this.stateService.blocks$
|
||||
this.$rewardStats = concat(
|
||||
// We fetch the latest reward stats when the page load and
|
||||
// wait for the API response before listening to websocket blocks
|
||||
this.apiService.getRewardStats$()
|
||||
.pipe(
|
||||
tap((stats) => {
|
||||
this.lastBlockHeight = stats.endBlock;
|
||||
})
|
||||
),
|
||||
// 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) {
|
||||
return []; // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = block[0].height;
|
||||
return this.apiService.getRewardStats$();
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
// (we always receives some blocks at start so only trigger for the last one)
|
||||
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
|
||||
switchMap(() => {
|
||||
return this.apiService.getRewardStats$()
|
||||
.pipe(
|
||||
map((stats) => {
|
||||
return {
|
||||
totalReward: stats.totalReward,
|
||||
rewardPerTx: stats.totalReward / stats.totalTx,
|
||||
feePerTx: stats.totalFee / stats.totalTx,
|
||||
};
|
||||
})
|
||||
);
|
||||
map((stats) => {
|
||||
return {
|
||||
totalReward: stats.totalReward,
|
||||
rewardPerTx: stats.totalReward / stats.totalTx,
|
||||
feePerTx: stats.totalFee / stats.totalTx,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div class="text-left">
|
||||
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bisq.markets/">bisq.markets</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://wq.apnic.net/static/search.html?query=AS142052">AS142052</a>.</p>
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bisq.markets/">bisq.markets</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
|
||||
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
|
||||
|
||||
<div class="title">
|
||||
<div class="title text-left">
|
||||
<h2 i18n="transaction.details">Details</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
|
||||
@@ -111,7 +111,10 @@
|
||||
<td style="text-align: left;" [innerHTML]="vin.inner_redeemscript_asm | asmStyler"></td>
|
||||
</tr>
|
||||
<tr *ngIf="vin.inner_witnessscript_asm">
|
||||
<td i18n="transactions-list.p2wsh-witness-script">P2WSH witness script</td>
|
||||
<td *ngIf="vin.prevout && vin.prevout.scriptpubkey_type == 'v1_p2tr'; else p2wsh" i18n="transactions-list.p2tr-tapscript">P2TR tapscript</td>
|
||||
<ng-template #p2wsh>
|
||||
<td i18n="transactions-list.p2wsh-witness-script">P2WSH witness script</td>
|
||||
</ng-template>
|
||||
<td style="text-align: left;" [innerHTML]="vin.inner_witnessscript_asm | asmStyler"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -155,6 +158,9 @@
|
||||
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-block">{{ vout.scriptpubkey_address | shortenString : 35 }}</span>
|
||||
</a>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout"></app-address-labels>
|
||||
</div>
|
||||
<ng-template #scriptpubkey_type>
|
||||
<ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
|
||||
<ng-container i18n="transactions-list.peg-out-to">Peg-out to <ng-container *ngTemplateOutlet="pegOutLink"></ng-container></ng-container>
|
||||
|
||||
Reference in New Issue
Block a user