Merge branch 'master' into simon/display-unblinded-tx-sum

This commit is contained in:
wiz 2022-02-23 03:09:44 +00:00 committed by GitHub
commit e617e14901
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 346 additions and 418 deletions

View File

@ -116,6 +116,9 @@ class Blocks {
Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0; Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
blockExtended.extras.feeRange = transactionsTmp.length > 0 ? blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
Common.getFeesInRange(transactionsTmp, 8) : [0, 0]; Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
blockExtended.extras.totalFees = transactionsTmp.reduce((acc, tx) => {
return acc + tx.fee;
}, 0)
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
let pool: PoolTag; let pool: PoolTag;

View File

@ -42,9 +42,7 @@ class Mining {
}); });
poolsStatistics['pools'] = poolsStats; poolsStatistics['pools'] = poolsStats;
poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
const blockCount: number = await BlocksRepository.$blockCount(null, interval); const blockCount: number = await BlocksRepository.$blockCount(null, interval);
poolsStatistics['blockCount'] = blockCount; poolsStatistics['blockCount'] = blockCount;
@ -79,26 +77,14 @@ class Mining {
* Return the historical difficulty adjustments and oldest indexed block timestamp * Return the historical difficulty adjustments and oldest indexed block timestamp
*/ */
public async $getHistoricalDifficulty(interval: string | null): Promise<object> { public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval); return await BlocksRepository.$getBlocksDifficulty(interval);
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
return {
adjustments: difficultyAdjustments,
oldestIndexedBlockTimestamp: oldestBlock.getTime(),
};
} }
/** /**
* Return the historical hashrates and oldest indexed block timestamp * Return the historical hashrates and oldest indexed block timestamp
*/ */
public async $getHistoricalHashrates(interval: string | null): Promise<object> { public async $getHistoricalHashrates(interval: string | null): Promise<object> {
const hashrates = await HashratesRepository.$get(interval); return await HashratesRepository.$get(interval);
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
return {
hashrates: hashrates,
oldestIndexedBlockTimestamp: oldestBlock.getTime(),
};
} }
/** /**
@ -175,7 +161,18 @@ class Mining {
++totalIndexed; ++totalIndexed;
} }
await HashratesRepository.$saveHashrates(hashrates); // Add genesis block manually
if (!indexedTimestamp.includes(genesisTimestamp)) {
hashrates.push({
hashrateTimestamp: genesisTimestamp,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null
});
}
if (hashrates.length > 0) {
await HashratesRepository.$saveHashrates(hashrates);
}
await HashratesRepository.$setLatestRunTimestamp(); await HashratesRepository.$setLatestRunTimestamp();
this.hashrateIndexingStarted = false; this.hashrateIndexingStarted = false;

View File

@ -78,6 +78,7 @@ export interface TransactionStripped {
} }
export interface BlockExtension { export interface BlockExtension {
totalFees?: number;
medianFee?: number; medianFee?: number;
feeRange?: number[]; feeRange?: number[];
reward?: number; reward?: number;

View File

@ -187,12 +187,11 @@ class BlocksRepository {
* Get the oldest indexed block * Get the oldest indexed block
*/ */
public async $oldestBlockTimestamp(): Promise<number> { public async $oldestBlockTimestamp(): Promise<number> {
const query = `SELECT blockTimestamp const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
FROM blocks FROM blocks
ORDER BY height ORDER BY height
LIMIT 1;`; LIMIT 1;`;
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(query); const [rows]: any[] = await connection.query(query);
@ -266,15 +265,37 @@ class BlocksRepository {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height // :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
FROM blocks`; // Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) { if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
} }
query += ` GROUP BY difficulty query += `
ORDER BY blockTimestamp`; ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
const [rows]: any[] = await connection.query(query); const [rows]: any[] = await connection.query(query);
connection.release(); connection.release();

View File

@ -588,11 +588,17 @@ class Routes {
public async $getHistoricalHashrate(req: Request, res: Response) { public async $getHistoricalHashrate(req: Request, res: Response) {
try { try {
const stats = await mining.$getHistoricalHashrates(req.params.interval ?? null); const hashrates = await mining.$getHistoricalHashrates(req.params.interval ?? null);
const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(stats); res.json({
oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
hashrates: hashrates,
difficulty: difficulty,
});
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

View File

@ -42,7 +42,28 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
pathRewrite: { pathRewrite: {
"^/liquid/api/": "/api/v1/" "^/liquid/api/": "/api/v1/"
}, },
} },
{
context: ['/liquidtestnet/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet": ""
},
},
{
context: ['/liquidtestnet/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet/api/": "/api/v1/"
},
},
]); ]);
} }

View File

@ -40,6 +40,24 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
changeOrigin: true, changeOrigin: true,
proxyTimeout: 30000, proxyTimeout: 30000,
}, },
{
context: ['/liquidtestnet/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet": ""
},
},
{
context: ['/liquidtestnet/api/**'],
target: `https://liquid.network`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
]); ]);
} }

View File

@ -28,7 +28,6 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
import { AssetsComponent } from './components/assets/assets.component'; import { AssetsComponent } from './components/assets/assets.component';
import { PoolComponent } from './components/pool/pool.component'; import { PoolComponent } from './components/pool/pool.component';
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component'; import { MiningStartComponent } from './components/mining-start/mining-start.component';
@ -75,10 +74,6 @@ let routes: Routes = [
path: 'mining', path: 'mining',
component: MiningStartComponent, component: MiningStartComponent,
children: [ children: [
{
path: 'difficulty',
component: DifficultyChartComponent,
},
{ {
path: 'hashrate', path: 'hashrate',
component: HashrateChartComponent, component: HashrateChartComponent,
@ -194,10 +189,6 @@ let routes: Routes = [
path: 'mining', path: 'mining',
component: MiningStartComponent, component: MiningStartComponent,
children: [ children: [
{
path: 'difficulty',
component: DifficultyChartComponent,
},
{ {
path: 'hashrate', path: 'hashrate',
component: HashrateChartComponent, component: HashrateChartComponent,
@ -307,10 +298,6 @@ let routes: Routes = [
path: 'mining', path: 'mining',
component: MiningStartComponent, component: MiningStartComponent,
children: [ children: [
{
path: 'difficulty',
component: DifficultyChartComponent,
},
{ {
path: 'hashrate', path: 'hashrate',
component: HashrateChartComponent, component: HashrateChartComponent,

View File

@ -70,7 +70,6 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component'; import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component'; import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
@ -126,7 +125,6 @@ import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
AssetGroupComponent, AssetGroupComponent,
AssetCirculationComponent, AssetCirculationComponent,
MiningDashboardComponent, MiningDashboardComponent,
DifficultyChartComponent,
HashrateChartComponent, HashrateChartComponent,
MiningStartComponent, MiningStartComponent,
AmountShortenerPipe, AmountShortenerPipe,

View File

@ -14,7 +14,7 @@
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</div> </div>
<div *ngIf="showMiningInfo" class="block-size"> <div *ngIf="showMiningInfo" class="block-size">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div> </div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div> <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<div class="transaction-count"> <div class="transaction-count">

View File

@ -1,53 +0,0 @@
<div [class]="widget === false ? 'container-xl' : ''">
<div class="card-header mb-0 mb-lg-4" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(difficultyObservable$ | async) as diffChanges">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 90">
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 180">
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 365">
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 730">
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 1095">
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" [routerLink]="['/mining/difficulty' | relativeUrl]" fragment="all"> ALL
</label>
</div>
</form>
</div>
<div *ngIf="difficultyObservable$ | async" class="mb-5" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
<table class="table table-borderless table-sm text-center" *ngIf="!widget">
<thead>
<tr>
<th i18n="mining.rank">Block</th>
<th i18n="block.timestamp">Timestamp</th>
<th i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change">Change</th>
</tr>
</thead>
<tbody *ngIf="(difficultyObservable$ | async) as diffChanges">
<tr *ngFor="let diffChange of diffChanges.data">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td>&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,27 +0,0 @@
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}

View File

@ -1,183 +0,0 @@
import { 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 { selectPowerOfTen } from 'src/app/bitcoin.utils';
@Component({
selector: 'app-difficulty-chart',
templateUrl: './difficulty-chart.component.html',
styleUrls: ['./difficulty-chart.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 38%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class DifficultyChartComponent implements OnInit {
@Input() widget: boolean = false;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg'
};
difficultyObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
) {
this.seoService.setTitle($localize`:@@mining.difficulty:Difficulty`);
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith('1y'),
switchMap((timespan) => {
return this.apiService.getHistoricalDifficulty$(timespan)
.pipe(
tap(data => {
this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty]));
this.isLoading = false;
}),
map(data => {
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
) / 3600 / 24;
const tableData = [];
for (let i = data.adjustments.length - 1; i > 0; --i) {
const selectedPowerOfTen: any = selectPowerOfTen(data.adjustments[i].difficulty);
const change = (data.adjustments[i].difficulty / data.adjustments[i - 1].difficulty - 1) * 100;
tableData.push(Object.assign(data.adjustments[i], {
change: change,
difficultyShorten: formatNumber(
data.adjustments[i].difficulty / selectedPowerOfTen.divider,
this.locale, '1.2-2') + selectedPowerOfTen.unit
}));
}
return {
availableTimespanDay: availableTimespanDay,
data: tableData
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
color: new graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#D81B60' },
{ offset: 0.25, color: '#8E24AA' },
{ offset: 0.5, color: '#5E35B1' },
{ offset: 0.75, color: '#3949AB' },
{ offset: 1, color: '#1E88E5' }
]),
title: {
text: this.widget? '' : $localize`:@@mining.difficulty:Difficulty`,
left: 'center',
textStyle: {
color: '#FFF',
},
},
tooltip: {
show: true,
trigger: 'axis',
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: params => {
return `<b style="color: white">${params[0].axisValueLabel}</b><br>
${params[0].marker} ${formatNumber(params[0].value[1], this.locale, '1.0-0')}`
}
},
axisPointer: {
type: 'line',
},
xAxis: {
type: 'time',
splitNumber: this.isMobile() ? 5 : 10,
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (val) => {
const selectedPowerOfTen: any = selectPowerOfTen(val);
const diff = val / selectedPowerOfTen.divider;
return `${diff} ${selectedPowerOfTen.unit}`;
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: {
showSymbol: false,
data: data,
type: 'line',
smooth: false,
lineStyle: {
width: 2,
},
},
dataZoom: this.widget ? null : [{
type: 'inside',
realtime: true,
zoomLock: true,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
maxSpan: 100,
minSpan: 10,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
bottom: 0,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
},
}],
};
}
isMobile() {
return (window.innerWidth <= 767.98);
}
}

View File

@ -25,9 +25,34 @@
</form> </form>
</div> </div>
<div *ngIf="hashrateObservable$ | async" [class]="widget === false ? 'chart' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div> <div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="mt-3" *ngIf="!widget">
<table class="table table-borderless table-sm text-center">
<thead>
<tr>
<th i18n="mining.rank">Block</th>
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
<th i18n="mining.adjusted">Adjusted</th>
<th i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change">Change</th>
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data.difficulty">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td class="d-none d-md-block">&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div>
</div> </div>

View File

@ -26,6 +26,11 @@
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 20px; padding-right: 20px;
} }
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
}
.formRadioGroup { .formRadioGroup {
margin-top: 6px; margin-top: 6px;

View File

@ -23,7 +23,7 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils';
}) })
export class HashrateChartComponent implements OnInit { export class HashrateChartComponent implements OnInit {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() right: number | string = 10; @Input() right: number | string = 45;
@Input() left: number | string = 75; @Input() left: number | string = 75;
radioGroupForm: FormGroup; radioGroupForm: FormGroup;
@ -45,7 +45,7 @@ export class HashrateChartComponent implements OnInit {
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
) { ) {
this.seoService.setTitle($localize`:@@mining.hashrate:hashrate`); this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y'); this.radioGroupForm.controls.dateSpan.setValue('1y');
} }
@ -57,17 +57,61 @@ export class HashrateChartComponent implements OnInit {
switchMap((timespan) => { switchMap((timespan) => {
return this.apiService.getHistoricalHashrate$(timespan) return this.apiService.getHistoricalHashrate$(timespan)
.pipe( .pipe(
tap(data => { tap((data: any) => {
this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate])); // We generate duplicated data point so the tooltip works nicely
const diffFixed = [];
let diffIndex = 1;
let hashIndex = 0;
while (hashIndex < data.hashrates.length) {
if (diffIndex >= data.difficulty.length) {
while (hashIndex < data.hashrates.length) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty[data.difficulty.length - 1].difficulty
});
++hashIndex;
}
break;
}
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].timestamp
) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty[diffIndex - 1].difficulty
});
++hashIndex;
}
++diffIndex;
}
this.prepareChartOptions({
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty])
});
this.isLoading = false; this.isLoading = false;
}), }),
map(data => { map((data: any) => {
const availableTimespanDay = ( const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000) (new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
) / 3600 / 24; ) / 3600 / 24;
const tableData = [];
for (let i = data.difficulty.length - 1; i > 0; --i) {
const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty);
const change = (data.difficulty[i].difficulty / data.difficulty[i - 1].difficulty - 1) * 100;
tableData.push(Object.assign(data.difficulty[i], {
change: change,
difficultyShorten: formatNumber(
data.difficulty[i].difficulty / selectedPowerOfTen.divider,
this.locale, '1.2-2') + selectedPowerOfTen.unit
}));
}
return { return {
availableTimespanDay: availableTimespanDay, availableTimespanDay: availableTimespanDay,
data: data.hashrates difficulty: tableData
}; };
}), }),
); );
@ -78,72 +122,144 @@ export class HashrateChartComponent implements OnInit {
prepareChartOptions(data) { prepareChartOptions(data) {
this.chartOptions = { this.chartOptions = {
color: new graphic.LinearGradient(0, 0, 0, 0.65, [ color: [
{ offset: 0, color: '#F4511E' }, new graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0.25, color: '#FB8C00' }, { offset: 0, color: '#F4511E' },
{ offset: 0.5, color: '#FFB300' }, { offset: 0.25, color: '#FB8C00' },
{ offset: 0.75, color: '#FDD835' }, { offset: 0.5, color: '#FFB300' },
{ offset: 1, color: '#7CB342' } { offset: 0.75, color: '#FDD835' },
]), { offset: 1, color: '#7CB342' }
]),
'#D81B60',
],
grid: { grid: {
right: this.right, right: this.right,
left: this.left, left: this.left,
}, bottom: this.widget ? 30 : 60,
title: {
text: this.widget ? '' : $localize`:@@mining.hashrate:Hashrate`,
left: 'center',
textStyle: {
color: '#FFF',
},
}, },
tooltip: { tooltip: {
show: true,
trigger: 'axis', trigger: 'axis',
axisPointer: {
type: 'line'
},
backgroundColor: 'rgba(17, 19, 31, 1)', backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4, borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)', shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: { textStyle: {
color: '#b1b1b1', color: '#b1b1b1',
align: 'left',
}, },
borderColor: '#000', borderColor: '#000',
formatter: params => { formatter: function (data) {
return `<b style="color: white">${params[0].axisValueLabel}</b><br> let hashratePowerOfTen: any = selectPowerOfTen(1);
${params[0].marker} ${formatNumber(params[0].value[1], this.locale, '1.0-0')} H/s` let hashrate = data[0].data[1];
} let difficultyPowerOfTen = hashratePowerOfTen;
}, let difficulty = data[1].data[1];
axisPointer: {
type: 'line', if (this.isMobile()) {
hashratePowerOfTen = selectPowerOfTen(data[0].data[1]);
hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider);
difficultyPowerOfTen = selectPowerOfTen(data[1].data[1]);
difficulty = Math.round(data[1].data[1] / difficultyPowerOfTen.divider);
}
return `
<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>
<span>${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s</span><br>
<span>${data[1].marker} ${data[1].seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}</span>
`;
}.bind(this)
}, },
xAxis: { xAxis: {
type: 'time', type: 'time',
splitNumber: this.isMobile() ? 5 : 10, splitNumber: this.isMobile() ? 5 : 10,
}, },
yAxis: { legend: {
type: 'value', data: [
axisLabel: { {
formatter: (val) => { name: 'Hashrate',
const selectedPowerOfTen: any = selectPowerOfTen(val); inactiveColor: 'rgb(110, 112, 121)',
const newVal = val / selectedPowerOfTen.divider; textStyle: {
return `${newVal} ${selectedPowerOfTen.unit}H/s` color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: '#FFB300',
},
},
{
name: 'Difficulty',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: '#D81B60',
}
},
],
},
yAxis: [
{
min: function (value) {
return value.min * 0.9;
},
type: 'value',
name: 'Hashrate',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
const selectedPowerOfTen: any = selectPowerOfTen(val);
const newVal = Math.round(val / selectedPowerOfTen.divider);
return `${newVal} ${selectedPowerOfTen.unit}H/s`
}
},
splitLine: {
show: false,
} }
}, },
splitLine: { {
min: function (value) {
return value.min * 0.9;
},
type: 'value',
name: 'Difficulty',
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
const selectedPowerOfTen: any = selectPowerOfTen(val);
const newVal = Math.round(val / selectedPowerOfTen.divider);
return `${newVal} ${selectedPowerOfTen.unit}`
}
},
splitLine: {
show: false,
}
}
],
series: [
{
name: 'Hashrate',
showSymbol: false,
data: data.hashrates,
type: 'line',
lineStyle: { lineStyle: {
type: 'dotted', width: 2,
color: '#ffffff66', },
opacity: 0.25, },
{
yAxisIndex: 1,
name: 'Difficulty',
showSymbol: false,
data: data.difficulty,
type: 'line',
lineStyle: {
width: 3,
} }
}, }
}, ],
series: {
showSymbol: false,
data: data,
type: 'line',
smooth: false,
lineStyle: {
width: 2,
},
},
dataZoom: this.widget ? null : [{ dataZoom: this.widget ? null : [{
type: 'inside', type: 'inside',
realtime: true, realtime: true,

View File

@ -13,7 +13,7 @@
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div> </div>
<div *ngIf="showMiningInfo" class="block-size"> <div *ngIf="showMiningInfo" class="block-size">
<app-amount [satoshis]="projectedBlock.totalFees + blockSubsidy * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-amount [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div> </div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div> <div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<div class="transaction-count"> <div class="transaction-count">

View File

@ -34,7 +34,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
network = ''; network = '';
now = new Date().getTime(); now = new Date().getTime();
showMiningInfo = false; showMiningInfo = false;
blockSubsidy = 50;
blockWidth = 125; blockWidth = 125;
blockPadding = 30; blockPadding = 30;
@ -111,7 +110,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
if (this.stateService.network === '') { if (this.stateService.network === '') {
block.blink = specialBlocks[block.height] ? true : false; block.blink = specialBlocks[block.height] ? true : false;
} }
this.setBlockSubsidy(block.height);
}); });
const stringifiedBlocks = JSON.stringify(mempoolBlocks); const stringifiedBlocks = JSON.stringify(mempoolBlocks);
@ -212,18 +210,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
return block.index; return block.index;
} }
setBlockSubsidy(blockHeight) {
if (!['', 'testnet', 'signet'].includes(this.stateService.network)) {
return;
}
this.blockSubsidy = 50;
let halvenings = Math.floor(blockHeight / 210000);
while (halvenings > 0) {
this.blockSubsidy = this.blockSubsidy / 2;
halvenings--;
}
}
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2; const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding))); const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));

View File

@ -5,11 +5,13 @@
<!-- pool distribution --> <!-- pool distribution -->
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body pool-ranking">
<h5 class="card-title" i18n="mining.pool-share">Mining Pools Share (1w)</h5> <h5 class="card-title">
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
Mining Pools Share (1w)
</a>
</h5>
<app-pool-ranking [widget]=true></app-pool-ranking> <app-pool-ranking [widget]=true></app-pool-ranking>
<div class="text-center"><a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div> </div>
</div> </div>
</div> </div>
@ -18,22 +20,12 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title" i18n="mining.hashrate">Hashrate (1y)</h5> <h5 class="card-title">
<a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
Hashrate (1y)
</a>
</h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart> <app-hashrate-chart [widget]=true></app-hashrate-chart>
<div class="text-center"><a href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
<!-- difficulty -->
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title" i18n="ning.difficulty">Difficulty (1y)</h5>
<app-difficulty-chart [widget]=true></app-difficulty-chart>
<div class="text-center"><a href="" [routerLink]="['/mining/difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,22 +16,17 @@
} }
.card-title { .card-title {
color: #4a68b9;
font-size: 1rem; font-size: 1rem;
} }
.card-title > a {
color: #4a68b9;
}
.card-wrapper { .card-body {
.card { padding: 1.25rem 1rem 0.75rem 1rem;
height: auto !important; }
} .card-body.pool-ranking {
.card-body { padding: 1.25rem 0.25rem 0.75rem 0.25rem;
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
}
} }
#blockchain-container { #blockchain-container {

View File

@ -1,11 +1,12 @@
<div [class]="widget === false ? 'container-xl' : ''"> <div [class]="widget === false ? 'container-xl' : ''">
<div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div> <div [class]="widget ? 'chart-widget' : 'chart'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="card-header mb-0 mb-lg-4" [style]="widget === true ? 'display:none' : ''"> <div class="card-header mb-0 mb-lg-4 mt-md-3" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">

View File

@ -1,10 +1,14 @@
.hashrate-pie { .chart {
height: 100%; max-height: 400px;
min-height: 400px;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
min-height: 300px; max-height: 300px;
} }
} }
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
}
.formRadioGroup { .formRadioGroup {
margin-top: 6px; margin-top: 6px;

View File

@ -33,7 +33,9 @@ export class PoolRankingComponent implements OnInit {
isLoading = true; isLoading = true;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
renderer: 'svg' renderer: 'svg',
width: 'auto',
height: 'auto',
}; };
chartInstance: any = undefined; chartInstance: any = undefined;
@ -156,6 +158,11 @@ export class PoolRankingComponent implements OnInit {
} }
network = network.charAt(0).toUpperCase() + network.slice(1); network = network.charAt(0).toUpperCase() + network.slice(1);
let radius: any[] = ['20%', '70%'];
if (this.isMobile() || this.widget) {
radius = ['20%', '65%'];
}
this.chartOptions = { this.chartOptions = {
title: { title: {
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`, text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
@ -169,17 +176,21 @@ export class PoolRankingComponent implements OnInit {
} }
}, },
tooltip: { tooltip: {
trigger: 'item' trigger: 'item',
textStyle: {
align: 'left',
}
}, },
series: [ series: [
{ {
top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'), top: this.widget ? 0 : 35,
bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
name: 'Mining pool', name: 'Mining pool',
type: 'pie', type: 'pie',
radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']), radius: radius,
data: this.generatePoolsChartSerieData(miningStats), data: this.generatePoolsChartSerieData(miningStats),
labelLine: { labelLine: {
length: this.isMobile() ? 10 : 15,
length2: this.isMobile() ? 0 : 15,
lineStyle: { lineStyle: {
width: 2, width: 2,
}, },

View File

@ -172,9 +172,12 @@
</td> </td>
<td class="text-right nowrap amount"> <td class="text-right nowrap amount">
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> <ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset]"> <div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container> <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
</div> </div>
<ng-template #assetNotFound>
{{ vout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vout.asset]">{{ vout.asset | slice : 0 : 7 }}</a>
</ng-template>
</ng-template> </ng-template>
<ng-template #defaultOutput> <ng-template #defaultOutput>
<app-amount [satoshis]="vout.value"></app-amount> <app-amount [satoshis]="vout.value"></app-amount>

View File

@ -101,6 +101,7 @@ export interface PoolStat {
} }
export interface BlockExtension { export interface BlockExtension {
totalFees?: number;
medianFee?: number; medianFee?: number;
feeRange?: number[]; feeRange?: number[];
reward?: number; reward?: number;

View File

@ -82,7 +82,7 @@ export class MiningService {
}); });
const availableTimespanDay = ( const availableTimespanDay = (
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp / 1000) (new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp)
) / 3600 / 24; ) / 3600 / 24;
return { return {