mirror of
https://github.com/mempool/mempool.git
synced 2024-12-29 09:44:26 +01:00
Merge branch 'master' into simon/footer-transactions-bar-fix
This commit is contained in:
commit
d05d7f1e27
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.$saveHashrates(hashrates);
|
||||||
|
}
|
||||||
await HashratesRepository.$setLatestRunTimestamp();
|
await HashratesRepository.$setLatestRunTimestamp();
|
||||||
this.hashrateIndexingStarted = false;
|
this.hashrateIndexingStarted = false;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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/"
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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]="'‎' + (block.size | bytes: 2)"></div>
|
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
|
@ -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>‎{{ 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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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">‎{{ 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>
|
||||||
|
@ -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;
|
||||||
|
@ -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: [
|
||||||
|
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||||
{ offset: 0, color: '#F4511E' },
|
{ offset: 0, color: '#F4511E' },
|
||||||
{ offset: 0.25, color: '#FB8C00' },
|
{ offset: 0.25, color: '#FB8C00' },
|
||||||
{ offset: 0.5, color: '#FFB300' },
|
{ offset: 0.5, color: '#FFB300' },
|
||||||
{ offset: 0.75, color: '#FDD835' },
|
{ offset: 0.75, color: '#FDD835' },
|
||||||
{ offset: 1, color: '#7CB342' }
|
{ 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];
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
axisPointer: {
|
return `
|
||||||
type: 'line',
|
<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: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'Hashrate',
|
||||||
|
inactiveColor: 'rgb(110, 112, 121)',
|
||||||
|
textStyle: {
|
||||||
|
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',
|
type: 'value',
|
||||||
|
name: 'Hashrate',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
|
color: 'rgb(110, 112, 121)',
|
||||||
formatter: (val) => {
|
formatter: (val) => {
|
||||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||||
const newVal = val / selectedPowerOfTen.divider;
|
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`
|
return `${newVal} ${selectedPowerOfTen.unit}H/s`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
show: false,
|
||||||
type: 'dotted',
|
|
||||||
color: '#ffffff66',
|
|
||||||
opacity: 0.25,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
min: function (value) {
|
||||||
|
return value.min * 0.9;
|
||||||
},
|
},
|
||||||
series: {
|
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,
|
showSymbol: false,
|
||||||
data: data,
|
data: data.hashrates,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: false,
|
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
yAxisIndex: 1,
|
||||||
|
name: 'Difficulty',
|
||||||
|
showSymbol: false,
|
||||||
|
data: data.difficulty,
|
||||||
|
type: 'line',
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
dataZoom: this.widget ? null : [{
|
dataZoom: this.widget ? null : [{
|
||||||
type: 'inside',
|
type: 'inside',
|
||||||
realtime: true,
|
realtime: true,
|
||||||
|
@ -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]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
|
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
|
@ -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)));
|
||||||
|
@ -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
|
|
||||||
»</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
|
|
||||||
»</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
|
|
||||||
»</a></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,22 +16,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
color: #4a68b9;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
.card-title > a {
|
||||||
|
color: #4a68b9;
|
||||||
|
}
|
||||||
|
|
||||||
.card-wrapper {
|
|
||||||
.card {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
.card-body {
|
.card-body {
|
||||||
display: flex;
|
padding: 1.25rem 1rem 0.75rem 1rem;
|
||||||
flex: inherit;
|
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding: 22px 20px;
|
|
||||||
}
|
}
|
||||||
|
.card-body.pool-ranking {
|
||||||
|
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blockchain-container {
|
#blockchain-container {
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
@ -254,7 +257,7 @@
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
||||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||||
<ng-template #defaultAmount>
|
<ng-template #defaultAmount>
|
||||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -95,6 +95,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
haveBlindedOutputValues(tx: Transaction): boolean {
|
||||||
|
return tx.vout.some((v: any) => v.value === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
getTotalTxOutput(tx: Transaction) {
|
getTotalTxOutput(tx: Transaction) {
|
||||||
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user