Merge branch 'master' into simon/sponsor-page-ux

This commit is contained in:
wiz
2022-04-14 20:51:49 +00:00
committed by GitHub
63 changed files with 2458 additions and 761 deletions

View File

@@ -33,6 +33,8 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/
import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { GraphsComponent } from './components/graphs/graphs.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component';
let routes: Routes = [
{
@@ -117,6 +119,14 @@ let routes: Routes = [
path: 'mining/pools',
component: PoolRankingComponent,
},
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
},
{
path: 'mining/block-rewards',
component: BlockRewardsGraphComponent,
}
],
},
{
@@ -127,13 +137,17 @@ let routes: Routes = [
path: 'docs/api/:type',
component: DocsComponent
},
{
path: 'docs/faq',
component: DocsComponent
},
{
path: 'docs/api',
redirectTo: 'docs/api/rest'
},
{
path: 'docs',
redirectTo: 'docs/api/rest'
redirectTo: 'docs/faq'
},
{
path: 'api',
@@ -207,18 +221,6 @@ let routes: Routes = [
path: 'blocks',
component: BlocksList,
},
{
path: 'hashrate',
component: HashrateChartComponent,
},
{
path: 'hashrate/pools',
component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'pool',
children: [
@@ -255,6 +257,14 @@ let routes: Routes = [
path: 'mining/pools',
component: PoolRankingComponent,
},
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
},
{
path: 'mining/block-rewards',
component: BlockRewardsGraphComponent,
}
]
},
{
@@ -266,13 +276,17 @@ let routes: Routes = [
path: 'docs/api/:type',
component: DocsComponent
},
{
path: 'docs/faq',
component: DocsComponent
},
{
path: 'docs/api',
redirectTo: 'docs/api/rest'
},
{
path: 'docs',
redirectTo: 'docs/api/rest'
redirectTo: 'docs/faq'
},
{
path: 'api',
@@ -343,18 +357,6 @@ let routes: Routes = [
path: 'blocks',
component: BlocksList,
},
{
path: 'hashrate',
component: HashrateChartComponent,
},
{
path: 'hashrate/pools',
component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'pool',
children: [
@@ -391,6 +393,14 @@ let routes: Routes = [
path: 'mining/pools',
component: PoolRankingComponent,
},
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
},
{
path: 'mining/block-rewards',
component: BlockRewardsGraphComponent,
}
]
},
{
@@ -402,13 +412,17 @@ let routes: Routes = [
path: 'docs/api/:type',
component: DocsComponent
},
{
path: 'docs/faq',
component: DocsComponent
},
{
path: 'docs/api',
redirectTo: 'docs/api/rest'
},
{
path: 'docs',
redirectTo: 'docs/api/rest'
redirectTo: 'docs/faq'
},
{
path: 'api',
@@ -507,19 +521,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'mempool',
component: StatisticsComponent,
},
{
path: 'mining/hashrate-difficulty',
component: HashrateChartComponent,
},
{
path: 'mining/pools-dominance',
component: HashrateChartPoolsComponent,
},
{
path: 'mining/pools',
component: PoolRankingComponent,
},
}
]
},
{
@@ -635,19 +637,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'mempool',
component: StatisticsComponent,
},
{
path: 'mining/hashrate-difficulty',
component: HashrateChartComponent,
},
{
path: 'mining/pools-dominance',
component: HashrateChartPoolsComponent,
},
{
path: 'mining/pools',
component: PoolRankingComponent,
},
}
]
},
{

View File

@@ -56,6 +56,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
import { ApiDocsComponent } from './components/docs/api-docs.component';
import { DocsComponent } from './components/docs/docs.component';
import { ApiDocsNavComponent } from './components/docs/api-docs-nav.component';
import { NoSanitizePipe } from './shared/pipes/no-sanitize.pipe';
import { CodeTemplateComponent } from './components/docs/code-template.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
@@ -80,6 +81,8 @@ import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RewardStatsComponent } from './components/reward-stats/reward-stats.component';
import { DataCyDirective } from './data-cy.directive';
import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component';
@NgModule({
declarations: [
@@ -119,6 +122,7 @@ import { DataCyDirective } from './data-cy.directive';
DashboardComponent,
DifficultyComponent,
ApiDocsComponent,
NoSanitizePipe,
CodeTemplateComponent,
TermsOfServiceComponent,
PrivacyPolicyComponent,
@@ -141,6 +145,8 @@ import { DataCyDirective } from './data-cy.directive';
BlocksList,
DataCyDirective,
RewardStatsComponent,
BlockFeesGraphComponent,
BlockRewardsGraphComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),

View File

@@ -52,7 +52,7 @@ export class AddressLabelsComponent implements OnInit {
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_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
} 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

View File

@@ -55,7 +55,7 @@
<br>
<div class="title-tx">
<h2>
<h2 class="text-left">
<ng-template [ngIf]="!transactions?.length">&nbsp;</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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -4411,3 +4411,177 @@ export const restApiDocsData = [
},
];
export const faqData = [
{
type: "category",
category: "basics",
fragment: "basics",
title: "Basics",
showConditions: bitcoinNetworks
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-a-mempool",
title: "What is a mempool?",
answer: "<p>A mempool (short for \"memory pool\") holds the queue of pending and unconfirmed transactions for a cryptocurrency network node. There is no one global mempool: every node on the network maintains its own mempool, so different nodes may hold different transactions in their mempools.</p>"
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-a-mempool-explorer",
title: "What is a mempool explorer?",
answer: "<p>A mempool explorer is a tool that enables you to view real-time and historical information about a node's mempool, visualize its transactions, and search and view those transactions.</p><p>The mempool.space website invented the concept of visualizing a Bitcoin node's mempool as <b>projected blocks</b>. These blocks are the inspiration for our half-filled block logo.</p><p>Projected blocks are on the left of the dotted white line, and confirmed blocks are on the right.</p>"
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-a-blockchain",
title: "What is a blockchain?",
answer: "<p>A blockchain is a distributed ledger that records the transactions for a cryptocurrency network. Miners amend the blockchain ledger by mining new blocks.</p>"
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-a-block-explorer",
title: "What is a block explorer?",
answer: "<p>A block explorer is a tool that enables you to explore real-time and historical information about the blockchain of a cryptocurrency. This includes data related to blocks, transactions, addresses, and more.</p>"
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-mining",
title: "What is mining?",
answer: "Mining is the process by which unconfirmed transactions in a mempool are confirmed into a block on a blockchain. Miners select unconfirmed transactions from their mempools and arrange them into a block such that they solve a particular math problem.</p><p>The first miner on the network to find a suitable block earns all the transaction fees from the transactions in that block. As a result, miners tend to prioritize transactions with higher transaction fees.</p>"
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-are-mining-pools",
title: "What are mining pools?",
answer: "Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks."
},
{
type: "category",
category: "help",
fragment: "help-stuck-transaction",
title: "Help! My transaction is stuck",
showConditions: bitcoinNetworks
},
{
type: "endpoint",
category: "help",
showConditions: bitcoinNetworks,
fragment: "why-is-transaction-stuck-in-mempool",
title: "Why is my transaction stuck in the mempool?",
answer: "<p>Miners decide which transactions are included in the blocks they mine, so they usually prioritize transactions which pay them the highest transaction fees (transaction fees are measured in sats per virtual byte, or sat/vB). If it's been a while and your transcation hasn't been confirmed, your transaction probably has a lower transaction fee relative to other transactions currently in the mempool.</p>"
},
{
type: "endpoint",
category: "help",
showConditions: bitcoinNetworks,
fragment: "how-to-get-transaction-confirmed-quickly",
title: "How can I get my transaction confirmed more quickly?",
answer: "<p>If your wallet supports RBF, and if your transaction was created with RBF enabled, you can bump the fee higher.</p><p>Otherwise, if your wallet does not support RBF, you can increase the effective fee rate of your transaction by spending its change output using a higher fee. This is called CPFP.</p>"
},
{
type: "endpoint",
category: "help",
showConditions: bitcoinNetworks,
fragment: "how-prevent-stuck-transaction",
title: "How can I prevent a transaction from getting stuck in the future?",
answer: "<p>You must use an adequate transaction fee commensurate with how quickly you need the transaction to be confirmed. Also consider using RBF if your wallet supports it so that you can bump the fee rate if needed.</p>"
},
{
type: "category",
category: "using",
fragment: "using-this-website",
title: "Using this website",
showConditions: bitcoinNetworks
},
{
type: "endpoint",
category: "how-to",
showConditions: bitcoinNetworks,
fragment: "looking-up-transactions",
title: "How can I look up a transaction?",
answer: "Search for the transaction ID in the search box at the top-right of this website."
},
{
type: "endpoint",
category: "how-to",
showConditions: bitcoinNetworks,
fragment: "looking-up-addresses",
title: "How can I look up an address?",
answer: "Search for the address in the search box at the top-right of this website."
},
{
type: "endpoint",
category: "how-to",
showConditions: bitcoinNetworks,
fragment: "looking-up-blocks",
title: "How can I look up a block?",
answer: "Search for the block number (or block hash) in the search box at the top-right of this website."
},
{
type: "endpoint",
category: "how-to",
showConditions: bitcoinNetworks,
fragment: "looking-up-fee-estimates",
title: "How can I look up fee estimates?",
answer: "<p>See real-time fee estimates on <a href='/'>the main dashboard</a>.</p><p>Low priority is suggested for confirmation within 6 blocks (~1 hour), Medium priority is suggested for confirmation within 3 blocks (~30 minutes), and High priority is suggested for confirmation in the next block (~10 minutes).</p>"
},
{
type: "endpoint",
category: "how-to",
showConditions: bitcoinNetworks,
fragment: "looking-up-historical-trends",
title: "How can I explore historical trends?",
answer: "See the <a href='/graphs'>graphs page</a> for aggregate trends over time: mempool size over time and incoming transaction velocity over time."
},
{
type: "category",
category: "advanced",
fragment: "advanced",
title: "Advanced",
showConditions: bitcoinNetworks
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "who-runs-this-website",
title: "Who runs this website?",
answer: "The official mempool.space website is operated by The Mempool Open Source Project. See more information on our <a href='/about'>About page</a>. There are also many unofficial instances of this website operated by individual members of the Bitcoin community."
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "host-my-own-instance-raspberry-pi",
title: "How can I host my own instance on a Raspberry Pi?",
answer: "We support one-click installation on a number of Raspberry Pi full-node distros including Umbrel, RaspiBlitz, MyNode, and RoninDojo."
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "host-my-own-instance-linux-server",
title: "How can I host my own instance on a Linux server?",
answer: "You can manually install mempool on your own Linux server, but this requires advanced sysadmin skills since you will be manually configuring everything. We do not provide support for manual deployments."
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "install-mempool-with-docker",
title: "Can I install Mempool using Docker?",
answer: "Yes, we publish Docker images (or you can build your own), and provide <a href='https://github.com/mempool/mempool/tree/master/docker' target='_blank'>an example docker-compose template</a>."
}
];

View File

@@ -1,4 +1,4 @@
<div *ngFor="let item of restDocs">
<div *ngFor="let item of tabData">
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</p>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
</div>

View File

@@ -1,5 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { restApiDocsData } from './api-docs-data';
import { faqData } from './api-docs-data';
@Component({
selector: 'app-api-docs-nav',
@@ -9,13 +10,18 @@ import { restApiDocsData } from './api-docs-data';
export class ApiDocsNavComponent implements OnInit {
@Input() network: any;
@Input() whichTab: string;
@Output() navLinkClickEvent: EventEmitter<any> = new EventEmitter();
restDocs: any[];
tabData: any[];
constructor() { }
ngOnInit(): void {
this.restDocs = restApiDocsData;
if( this.whichTab === 'rest' ) {
this.tabData = restApiDocsData;
} else if( this.whichTab = 'faq' ) {
this.tabData = faqData;
}
}
navLinkClick( event ) {

View File

@@ -1,20 +1,45 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<div class="container-xl text-left">
<div id="restAPI" *ngIf="restTabActivated">
<div id="faq" *ngIf="whichTab === 'faq'">
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }"></app-api-docs-nav>
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
</div>
<div class="doc-content">
<div class="doc-item-container" *ngFor="let item of faq">
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a>
<div class="endpoint-content">
<div class="endpoint" [innerHTML]="item.answer | noSanitize"></div>
<div class="blockchain-wrapper" *ngIf="item.fragment === 'what-is-a-mempool-explorer'">
<app-blockchain></app-blockchain>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="restAPI" *ngIf="whichTab === 'rest'">
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
</div>
<div class="doc-content">
<p class="hide-on-mobile no-bottom-space">Reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">API service</ng-container>.</p>
<div *ngFor="let item of restDocs">
<div class="doc-item-container" *ngFor="let item of restDocs">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
<a class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}">{{ item.title }} <span>{{ item.category }}</span></a>
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}">{{ item.title }} <span>{{ item.category }}</span></a>
<div class="endpoint-content">
<div class="endpoint">
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
@@ -65,7 +90,7 @@
</div>
</div>
<div id="websocketAPI" *ngIf="!restTabActivated && ( network.val !== 'bisq' )">
<div id="websocketAPI" *ngIf="( whichTab === 'websocket' ) && ( network.val !== 'bisq' )">
<div class="api-category">
<div class="websocket">
<div class="endpoint">

View File

@@ -152,6 +152,14 @@ h3 {
float: right;
}
.endpoint-container .section-header table {
width: 100%;
}
.endpoint-container .section-header table td:first-child {
padding-right: 24px;
}
#doc-nav-mobile {
position: fixed;
top: 20px;
@@ -179,6 +187,16 @@ h3 {
border-radius: 0.5rem 0.5rem 0 0;
}
.blockchain-wrapper {
position: relative;
width: 100%;
overflow: auto;
scrollbar-width: none;
}
.blockchain-wrapper::-webkit-scrollbar {
display: none;
}
@media (max-width: 992px) {
.hide-on-mobile {
@@ -231,4 +249,8 @@ h3 {
h3 {
display: none;
}
.doc-item-container:last-of-type .endpoint-container {
margin-bottom: 4rem;
}
}

View File

@@ -4,7 +4,7 @@ import { Observable, merge, of } from 'rxjs';
import { SeoService } from 'src/app/services/seo.service';
import { tap } from 'rxjs/operators';
import { ActivatedRoute } from "@angular/router";
import { restApiDocsData, wsApiDocsData } from './api-docs-data';
import { faqData, restApiDocsData, wsApiDocsData } from './api-docs-data';
@Component({
selector: 'app-api-docs',
@@ -18,8 +18,9 @@ export class ApiDocsComponent implements OnInit {
env: Env;
code: any;
baseNetworkUrl = '';
@Input() restTabActivated: Boolean;
@Input() whichTab: string;
desktopDocsNavPosition = "relative";
faq: any[];
restDocs: any[];
wsDocs: any;
screenWidth: number;
@@ -33,7 +34,9 @@ export class ApiDocsComponent implements OnInit {
ngAfterViewInit() {
const that = this;
setTimeout( () => {
this.openEndpointContainer( this.route.snapshot.fragment );
if( this.route.snapshot.fragment ) {
this.openEndpointContainer( this.route.snapshot.fragment );
}
window.addEventListener('scroll', function() {
that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
});
@@ -62,6 +65,7 @@ export class ApiDocsComponent implements OnInit {
this.hostname = `${document.location.protocol}//${this.hostname}`;
this.faq = faqData;
this.restDocs = restApiDocsData;
this.wsDocs = wsApiDocsData;
@@ -71,7 +75,16 @@ export class ApiDocsComponent implements OnInit {
}
anchorLinkClick( event: any ) {
const targetId = event.target.hash.substring(1);
let targetId = "";
if( event.target.nodeName === "A" ) {
targetId = event.target.hash.substring(1);
} else {
let element = event.target;
while( element.nodeName !== "A" ) {
element = element.parentElement;
}
targetId = element.hash.substring(1);
}
if( this.route.snapshot.fragment === targetId ) {
document.getElementById( targetId ).scrollIntoView();
}
@@ -79,7 +92,8 @@ export class ApiDocsComponent implements OnInit {
}
openEndpointContainer( targetId ) {
if( ( window.innerWidth <= 992 ) && this.restTabActivated && targetId ) {
const tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) {
const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
const endPointContentElHeight = endpointContentEl.clientHeight;
@@ -90,8 +104,8 @@ export class ApiDocsComponent implements OnInit {
endpointContentEl.style.opacity = "0";
endpointContentEl.classList.remove( "open" );
} else {
endpointContainerEl.style.height = endPointContentElHeight + 90 + "px";
endpointContentEl.style.top = "90px";
endpointContainerEl.style.height = endPointContentElHeight + tabHeaderHeight + 28 + "px";
endpointContentEl.style.top = tabHeaderHeight + 28 + "px";
endpointContentEl.style.opacity = "1";
endpointContentEl.classList.add( "open" );
}

View File

@@ -5,20 +5,29 @@
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
<li [ngbNavItem]="0">
<a ngbNavLink routerLink="../rest">API - REST</a>
<li [ngbNavItem]="0" *ngIf="showFaqTab">
<a ngbNavLink [routerLink]="['/docs/faq' | relativeUrl]">FAQ</a>
<ng-template ngbNavContent>
<app-api-docs [restTabActivated]="true"></app-api-docs>
<app-api-docs [whichTab]="'faq'"></app-api-docs>
</ng-template>
</li>
<li [ngbNavItem]="1" *ngIf="showWebSocketTab">
<a ngbNavLink routerLink="../websocket">API - WebSocket</a>
<li [ngbNavItem]="1">
<a ngbNavLink [routerLink]="['/docs/api/rest' | relativeUrl]">API - REST</a>
<ng-template ngbNavContent>
<app-api-docs [restTabActivated]="false"></app-api-docs>
<app-api-docs [whichTab]="'rest'"></app-api-docs>
</ng-template>
</li>
<li [ngbNavItem]="2" *ngIf="showWebSocketTab">
<a ngbNavLink [routerLink]="['/docs/api/websocket' | relativeUrl]">API - WebSocket</a>
<ng-template ngbNavContent>
<app-api-docs [whichTab]="'websocket'"></app-api-docs>
</ng-template>
</li>

View File

@@ -1,6 +1,7 @@
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';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-docs',
@@ -12,17 +13,31 @@ export class DocsComponent implements OnInit {
activeTab = 0;
env: Env;
showWebSocketTab = true;
showFaqTab = true;
@HostBinding('attr.dir') dir = 'ltr';
constructor(
private route: ActivatedRoute,
private stateService: StateService,
private websocket: WebsocketService,
) { }
ngOnInit(): void {
this.websocket.want(['blocks']);
const url = this.route.snapshot.url;
this.activeTab = ( url[2].path === "rest" ) ? 0 : 1;
if( url[1].path === "faq" ) {
this.activeTab = 0;
} else if( url[2].path === "rest" ) {
this.activeTab = 1;
} else {
this.activeTab = 2;
}
this.env = this.stateService.env;
this.showWebSocketTab = ( ! ( ( this.env.BASE_MODULE === "bisq" ) || ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
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";
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -19,26 +19,29 @@
</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>
@@ -67,4 +70,4 @@
</p>
</div>
</div>
</ng-template>
</ng-template>

View File

@@ -132,4 +132,4 @@
display: block;
max-width: 80px;
margin: 15px auto 3px;
}
}

View File

@@ -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,6 +30,7 @@ export class HashrateChartComponent implements OnInit {
@Input() right: number | string = 45;
@Input() left: number | string = 75;
miningWindowPreference: string;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
@@ -35,6 +38,8 @@ export class HashrateChartComponent implements OnInit {
renderer: 'svg',
};
@HostBinding('attr.dir') dir = 'ltr';
hashrateObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
@@ -45,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(
@@ -210,6 +227,9 @@ export class HashrateChartComponent implements OnInit {
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: [
@@ -248,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: {
@@ -276,6 +296,7 @@ export class HashrateChartComponent implements OnInit {
],
series: data.hashrates.length === 0 ? [] : [
{
zlevel: 0,
name: 'Hashrate',
showSymbol: false,
symbol: 'none',
@@ -286,6 +307,7 @@ export class HashrateChartComponent implements OnInit {
},
},
{
zlevel: 1,
yAxisIndex: 1,
name: 'Difficulty',
showSymbol: false,

View File

@@ -1,32 +1,35 @@
<div [class]="widget === false ? 'full-container' : ''">
<div class="full-container">
<div class="card-header mb-0 mb-md-4" [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>

View File

@@ -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,10 +24,10 @@ 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 = {};
@@ -33,6 +35,8 @@ export class HashrateChartPoolsComponent implements OnInit {
renderer: 'svg',
};
@HostBinding('attr.dir') dir = 'ltr';
hashrateObservable$: Observable<any>;
isLoading = true;
@@ -42,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(
@@ -73,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,
@@ -82,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,
@@ -155,11 +169,11 @@ export class HashrateChartPoolsComponent implements OnInit {
grid: {
right: this.right,
left: this.left,
bottom: this.widget ? 30 : 70,
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'
@@ -186,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 : {
@@ -205,7 +222,7 @@ export class HashrateChartPoolsComponent implements OnInit {
min: 0,
},
series: data.series,
dataZoom: this.widget ? null : [{
dataZoom: [{
type: 'inside',
realtime: true,
zoomLock: true,

View File

@@ -157,6 +157,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
},
series: [
{
zlevel: 0,
data: this.data.series[0],
type: 'line',
smooth: false,

View File

@@ -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',

View File

@@ -27,6 +27,11 @@ export class MinerComponent implements OnChanges {
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();
}

View File

@@ -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>
@@ -79,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>
@@ -90,7 +90,7 @@
<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.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.poolsWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
<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>
@@ -99,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

View File

@@ -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';
@@ -19,9 +19,9 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PoolRankingComponent implements OnInit {
@Input() widget: boolean = false;
@Input() widget = false;
poolsWindowPreference: string;
miningWindowPreference: string;
radioGroupForm: FormGroup;
isLoading = true;
@@ -31,6 +31,8 @@ export class PoolRankingComponent implements OnInit {
};
chartInstance: any = undefined;
@HostBinding('attr.dir') dir = 'ltr';
miningStatsObservable$: Observable<MiningStats>;
constructor(
@@ -46,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([
@@ -65,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;
})
)
])
@@ -78,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()))
);
@@ -148,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`;
@@ -184,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`;
@@ -212,6 +214,7 @@ export class PoolRankingComponent implements OnInit {
},
series: [
{
zlevel: 0,
minShowLabelAngle: 3.6,
name: 'Mining pool',
type: 'pie',

View File

@@ -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'] }}"
@@ -10,22 +11,21 @@
<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>
<!-- Regexes desktop -->
<tr *ngIf="!isMobile()">
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.tags">Tags</td>
<td *ngIf="poolStats.pool.regexes.length else nodata">
{{ poolStats.pool.regexes }}
<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 i18n="mining.tags" class="label">Tags</span>
<span class="label" i18n="mining.tags">Tags</span>
<div *ngIf="poolStats.pool.regexes.length else nodatamobile" class="overflow-auto">
{{ poolStats.pool.regexes }}
</div>
@@ -33,32 +33,35 @@
</tr>
<!-- Addresses desktop -->
<tr *ngIf="!isMobile()">
<td class="label" i18n="mining.addresses">Addresses</td>
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-bottom: 0;">
<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>
<button *ngIf="poolStats.pool.addresses.length >= 2" style="transform: translateY(-3px);"
type="button" class="btn btn-outline-info btn-sm float-right" (click)="collapse.toggle()"
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
</button>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
[routerLink]="['/address' | relativeUrl, address]">{{
address }}<br></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-outline-info btn-sm float-right small-button" (click)="collapse.toggle()"
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>
@@ -77,105 +80,198 @@
</table>
</div>
<div class="col-lg-3">
<div class="col-lg-6">
<table class="table table-borderless table-striped">
<tbody>
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()">
<td class="label" i18n="mining.mined-blocks">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>
<!-- Mined blocks desktop -->
<!-- Hashrate mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<div>{{ formatNumber(poolStats.blockCount, this.locale, '1.0-0') }}</div>
<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>
<!-- Empty blocks desktop -->
<tr *ngIf="!isMobile()">
<td class="label" i18n="mining.empty-blocks">Empty Blocks</td>
<td class="data">{{ formatNumber(poolStats.emptyBlocks, this.locale, '1.0-0') }}</td>
<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>
<!-- Empty blocks mobile -->
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan="2">
<span class="label" i18n="mining.empty-blocks">Empty Blocks</span>
<div>{{ formatNumber(poolStats.emptyBlocks, this.locale, '1.0-0') }}</div>
<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>~</td>
<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>
<!-- Blocks list -->
<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">
&lrm;{{ 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 *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">
&lrm;{{ 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">
@@ -209,6 +305,7 @@
</div>
<!-- Main table skeleton -->
<ng-template #loadingMain>
<div>
<div class="mb-3" style="display:flex; position: relative">
@@ -220,18 +317,18 @@
<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>
<!-- Regexes desktop -->
<tr *ngIf="!isMobile()">
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.tags">Tags</td>
<td>
<td style="vertical-align: middle">
<div class="skeleton-loader"></div>
</td>
</tr>
<!-- Regexes mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
@@ -243,71 +340,149 @@
</tr>
<!-- Addresses desktop -->
<tr *ngIf="!isMobile()">
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.addresses">Addresses</td>
<td>
<td style="vertical-align: middle;">
<div class="skeleton-loader"></div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
<div class="skeleton-loader"></div>
</div>
</td>
<ng-template #nodata>
<td>~</td>
</ng-template>
</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">
<div class="col-lg-6">
<table class="table table-borderless table-striped">
<tbody>
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<!-- Hashrate desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
<td class="data">
<div class="skeleton-loader"></div>
<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>
<!-- Mined blocks desktop -->
<!-- Hashrate mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<div>
<div class="skeleton-loader"></div>
</div>
<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>
<!-- Empty blocks desktop -->
<tr *ngIf="!isMobile()">
<td class="label" i18n="mining.empty-blocks">Empty Blocks</td>
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<td class="data">
<div class="skeleton-loader"></div>
<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>
<!-- Empty blocks mobile -->
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan="2">
<span class="label" i18n="mining.empty-blocks">Empty Blocks</span>
<div>
<div class="skeleton-loader"></div>
</div>
<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>

View File

@@ -36,6 +36,7 @@
@media (max-width: 768px) {
margin-bottom: 10px;
}
height: 400px;
}
div.scrollable {
@@ -49,18 +50,28 @@ div.scrollable {
.box {
padding-bottom: 5px;
@media (min-width: 767.98px) {
min-height: 187px;
}
}
.label {
width: 30%;
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: right;
padding-left: 25%;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
@@ -114,10 +125,6 @@ div.scrollable {
}
}
.fees {
width: 0%;
}
.size {
width: 12%;
@media (max-width: 1000px) {
@@ -146,6 +153,10 @@ div.scrollable {
.skeleton-loader {
max-width: 200px;
}
.skeleton-loader.data {
max-width: 70px;
}
.loadingGraphs {
position: absolute;
@@ -159,8 +170,38 @@ div.scrollable {
.small-button {
height: 20px;
transform: translateY(-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;
}

View File

@@ -8,6 +8,7 @@ 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',
@@ -41,6 +42,7 @@ export class PoolComponent implements OnInit {
private apiService: ApiService,
private route: ActivatedRoute,
public stateService: StateService,
private seoService: SeoService,
) {
}
@@ -66,6 +68,7 @@ export class PoolComponent implements OnInit {
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 + '", "';
@@ -73,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);
@@ -97,7 +104,21 @@ export class PoolComponent implements OnInit {
}
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, [
@@ -146,6 +167,9 @@ export class PoolComponent implements OnInit {
xAxis: {
type: 'time',
splitNumber: (this.isMobile()) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
yAxis: [
{
@@ -168,6 +192,7 @@ export class PoolComponent implements OnInit {
],
series: [
{
zlevel: 0,
name: 'Hashrate',
showSymbol: false,
symbol: 'none',
@@ -178,7 +203,7 @@ export class PoolComponent implements OnInit {
},
},
],
dataZoom: [{
dataZoom: data.length === 0 ? undefined : [{
type: 'inside',
realtime: true,
zoomLock: true,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -93,8 +93,19 @@ export interface PoolInfo {
}
export interface PoolStat {
pool: PoolInfo;
blockCount: number;
emptyBlocks: number;
blockCount: {
all: number,
'24h': number,
'1w': number,
};
blockShare: {
all: number,
'24h': number,
'1w': number,
};
estimatedHashrate: number;
reportedHashrate: number;
luck?: number;
}
export interface BlockExtension {

View File

@@ -168,6 +168,20 @@ export class ApiService {
);
}
getHistoricalBlockFees$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/fees` +
(interval !== undefined ? `/${interval}` : '')
);
}
getHistoricalBlockRewards$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/rewards` +
(interval !== undefined ? `/${interval}` : '')
);
}
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
}

View File

@@ -4,6 +4,7 @@ import { map } from 'rxjs/operators';
import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface';
import { ApiService } from '../services/api.service';
import { StateService } from './state.service';
import { StorageService } from './storage.service';
export interface MiningUnits {
hashrateDivider: number;
@@ -28,8 +29,12 @@ export class MiningService {
constructor(
private stateService: StateService,
private apiService: ApiService,
private storageService: StorageService,
) { }
/**
* Generate pool ranking stats
*/
public getMiningStats(interval: string): Observable<MiningStats> {
return this.apiService.listPools$(interval).pipe(
map(pools => this.generateMiningStats(pools))
@@ -63,6 +68,20 @@ export class MiningService {
};
}
/**
* Get the default selection timespan, cap with `min`
*/
public getDefaultTimespan(min: string): string {
const timespans = [
'24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'
];
const preference = this.storageService.getValue('miningWindowPreference') ?? '1w';
if (timespans.indexOf(preference) < timespans.indexOf(min)) {
return min;
}
return preference;
}
private generateMiningStats(stats: PoolsStats): MiningStats {
const miningUnits = this.getMiningUnits();
const hashrateDivider = miningUnits.hashrateDivider;

View File

@@ -7,21 +7,21 @@ import { Router, ActivatedRoute } from '@angular/router';
export class StorageService {
constructor(private router: Router, private route: ActivatedRoute) {
this.setDefaultValueIfNeeded('graphWindowPreference', '2h');
this.setDefaultValueIfNeeded('poolsWindowPreference', '1w');
this.setDefaultValueIfNeeded('miningWindowPreference', '1w');
}
setDefaultValueIfNeeded(key: string, defaultValue: string) {
let graphWindowPreference: string = this.getValue(key);
const graphWindowPreference: string = this.getValue(key);
if (graphWindowPreference === null) { // First visit to mempool.space
if (this.router.url.includes('graphs') && key === 'graphWindowPreference' ||
this.router.url.includes('pools') && key === 'poolsWindowPreference'
this.router.url.includes('pools') && key === 'miningWindowPreference'
) {
this.setValue(key, this.route.snapshot.fragment ? this.route.snapshot.fragment : defaultValue);
} else {
this.setValue(key, defaultValue);
}
} else if (this.router.url.includes('graphs') && key === 'graphWindowPreference' ||
this.router.url.includes('pools') && key === 'poolsWindowPreference'
this.router.url.includes('pools') && key === 'miningWindowPreference'
) {
// Visit a different graphs#fragment from last visit
if (this.route.snapshot.fragment !== null && graphWindowPreference !== this.route.snapshot.fragment) {

View File

@@ -4,8 +4,9 @@ import { Pipe, PipeTransform } from '@angular/core';
name: 'amountShortener'
})
export class AmountShortenerPipe implements PipeTransform {
transform(num: number, ...args: number[]): unknown {
transform(num: number, ...args: any[]): unknown {
const digits = args[0] || 1;
const unit = args[1] || undefined;
if (num < 1000) {
return num.toFixed(digits);
@@ -21,7 +22,12 @@ export class AmountShortenerPipe implements PipeTransform {
{ value: 1e18, symbol: 'E' }
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
var item = lookup.slice().reverse().find((item) => num >= item.value);
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
const item = lookup.slice().reverse().find((item) => num >= item.value);
if (unit !== undefined) {
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + ' ' + item.symbol + unit : '0';
} else {
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
}
}
}

View File

@@ -281,6 +281,7 @@ export class AsmStylerPipe implements PipeTransform {
case 'CHECKSIGVERIFY':
case 'CHECKMULTISIG':
case 'CHECKMULTISIGVERIFY':
case 'CHECKSIGADD':
style = 'crypto';
break;

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({ name: 'noSanitize' })
export class NoSanitizePipe implements PipeTransform {
constructor(private domSanitizer: DomSanitizer) { }
transform(html: string): SafeHtml {
return this.domSanitizer.bypassSecurityTrustHtml(html);
}
}

View File

@@ -698,6 +698,16 @@ th {
margin-right: 0px;
text-align: right;
}
.nav-pills {
@extend .nav-pills;
display: inline-block;
}
.description {
direction: rtl;
}
.dropdown {
margin-right: 1rem;
margin-left: 0;
@@ -712,12 +722,29 @@ th {
left: 0px;
right: auto;
}
.fa-arrow-alt-circle-right {
@extend .fa-arrow-alt-circle-right;
.fa-circle-right {
@extend .fa-circle-right;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.btn.ml-2 {
margin-right: 0.5rem !important;
}
.pool-name {
@extend .pool-name;
padding-right: 10px;
}
.endpoint-container {
@extend .endpoint-container;
.section-header {
@extend .section-header;
text-align: left;
}
}
.table td {
text-align: right;
.fiat {
@@ -809,6 +836,14 @@ th {
}
}
.full-container {
@extend .full-container;
.formRadioGroup {
@extend .formRadioGroup;
direction: ltr;
}
}
.mempool-graph {
@extend .mempool-graph;
direction: ltr;