diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts
index 2829e5df1..6b7d2e01d 100644
--- a/backend/src/api/mining.ts
+++ b/backend/src/api/mining.ts
@@ -18,23 +18,20 @@ class Mining {
* Get historical block reward and total fee
*/
public async $getHistoricalBlockFees(interval: string | null = null): Promise {
- let timeRange: number;
- switch (interval) {
- case '3y': timeRange = 43200; break; // 12h
- case '2y': timeRange = 28800; break; // 8h
- case '1y': timeRange = 28800; break; // 8h
- case '6m': timeRange = 10800; break; // 3h
- case '3m': timeRange = 7200; break; // 2h
- case '1m': timeRange = 1800; break; // 30min
- case '1w': timeRange = 300; break; // 5min
- case '3d': timeRange = 1; break;
- case '24h': timeRange = 1; break;
- default: timeRange = 86400; break; // 24h
- }
+ return await BlocksRepository.$getHistoricalBlockFees(
+ this.getTimeRange(interval),
+ Common.getSqlInterval(interval)
+ );
+ }
- interval = Common.getSqlInterval(interval);
-
- return await BlocksRepository.$getHistoricalBlockFees(timeRange, interval);
+ /**
+ * Get historical block rewards
+ */
+ public async $getHistoricalBlockRewards(interval: string | null = null): Promise {
+ return await BlocksRepository.$getHistoricalBlockRewards(
+ this.getTimeRange(interval),
+ Common.getSqlInterval(interval)
+ );
}
/**
@@ -345,6 +342,21 @@ class Mining {
return date;
}
+
+ private getTimeRange(interval: string | null): number {
+ switch (interval) {
+ case '3y': return 43200; // 12h
+ case '2y': return 28800; // 8h
+ case '1y': return 28800; // 8h
+ case '6m': return 10800; // 3h
+ case '3m': return 7200; // 2h
+ case '1m': return 1800; // 30min
+ case '1w': return 300; // 5min
+ case '3d': return 1;
+ case '24h': return 1;
+ default: return 86400; // 24h
+ }
+ }
}
export default new Mining();
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 591afcfb4..c8e98c0b7 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -317,6 +317,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
+ .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
;
}
diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts
index a58d689e9..2ec97ce88 100644
--- a/backend/src/repositories/BlocksRepository.ts
+++ b/backend/src/repositories/BlocksRepository.ts
@@ -468,6 +468,35 @@ class BlocksRepository {
throw e;
}
}
+
+ /**
+ * Get the historical averaged block rewards
+ */
+ public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise {
+ let connection;
+ try {
+ connection = await DB.getConnection();
+
+ let query = `SELECT CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
+ CAST(AVG(reward) as INT) as avg_rewards
+ FROM blocks`;
+
+ if (interval !== null) {
+ query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
+ }
+
+ query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
+
+ const [rows]: any = await connection.query(query);
+ connection.release();
+
+ return rows;
+ } catch (e) {
+ connection.release();
+ logger.err('$getHistoricalBlockRewards() error: ' + (e instanceof Error ? e.message : e));
+ throw e;
+ }
+ }
}
export default new BlocksRepository();
diff --git a/backend/src/routes.ts b/backend/src/routes.ts
index c2ddac72c..9d4adb796 100644
--- a/backend/src/routes.ts
+++ b/backend/src/routes.ts
@@ -654,6 +654,22 @@ class Routes {
}
}
+ public async $getHistoricalBlockRewards(req: Request, res: Response) {
+ try {
+ const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval ?? null);
+ const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
+ res.header('Pragma', 'public');
+ res.header('Cache-control', 'public');
+ res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
+ res.json({
+ oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
+ blockRewards: blockRewards,
+ });
+ } catch (e) {
+ res.status(500).send(e instanceof Error ? e.message : e);
+ }
+ }
+
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 0ff1ee006..57f91b946 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -34,6 +34,7 @@ import { MiningStartComponent } from './components/mining-start/mining-start.com
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 = [
{
@@ -121,6 +122,10 @@ let routes: Routes = [
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
+ },
+ {
+ path: 'mining/block-rewards',
+ component: BlockRewardsGraphComponent,
}
],
},
@@ -255,6 +260,10 @@ let routes: Routes = [
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
+ },
+ {
+ path: 'mining/block-rewards',
+ component: BlockRewardsGraphComponent,
}
]
},
@@ -383,6 +392,10 @@ let routes: Routes = [
{
path: 'mining/block-fees',
component: BlockFeesGraphComponent,
+ },
+ {
+ path: 'mining/block-rewards',
+ component: BlockRewardsGraphComponent,
}
]
},
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 4536a2ff1..470284591 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -81,6 +81,7 @@ 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: [
@@ -143,6 +144,7 @@ import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fee
DataCyDirective,
RewardStatsComponent,
BlockFeesGraphComponent,
+ BlockRewardsGraphComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html
new file mode 100644
index 000000000..c2a3bcf00
--- /dev/null
+++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.html
@@ -0,0 +1,64 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss
new file mode 100644
index 000000000..54dbe5fad
--- /dev/null
+++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.scss
@@ -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;
+}
diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts
new file mode 100644
index 000000000..a22617922
--- /dev/null
+++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts
@@ -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;
+ 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 `
+ ${tick.axisValueLabel}
+ ${rewardsString}
+ `;
+ }
+ },
+ 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);
+ }
+}
diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html
index 97654beb5..e3bdb0629 100644
--- a/frontend/src/app/components/graphs/graphs.component.html
+++ b/frontend/src/app/components/graphs/graphs.component.html
@@ -20,8 +20,12 @@
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">
Block Fees
+
+ Block Rewards
+
-
\ No newline at end of file
+
diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html
index 49004c225..93f17dcdf 100644
--- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html
+++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html
@@ -70,4 +70,4 @@
-
\ No newline at end of file
+
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index 4011993b2..16a8d21d5 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -173,7 +173,14 @@ export class ApiService {
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/fees` +
(interval !== undefined ? `/${interval}` : '')
);
-}
+ }
+
+ getHistoricalBlockRewards$(interval: string | undefined) : Observable {
+ return this.httpClient.get(
+ this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/rewards` +
+ (interval !== undefined ? `/${interval}` : '')
+ );
+ }
getRewardStats$(blockCount: number = 144): Observable {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer
index 21824b80b..3c3204493 100755
--- a/production/nginx-cache-warmer
+++ b/production/nginx-cache-warmer
@@ -47,6 +47,16 @@ do for url in / \
'/api/v1/mining/blocks/fees/2y' \
'/api/v1/mining/blocks/fees/3y' \
'/api/v1/mining/blocks/fees/all' \
+ '/api/v1/mining/blocks/rewards/24h' \
+ '/api/v1/mining/blocks/rewards/3d' \
+ '/api/v1/mining/blocks/rewards/1w' \
+ '/api/v1/mining/blocks/rewards/1m' \
+ '/api/v1/mining/blocks/rewards/3m' \
+ '/api/v1/mining/blocks/rewards/6m' \
+ '/api/v1/mining/blocks/rewards/1y' \
+ '/api/v1/mining/blocks/rewards/2y' \
+ '/api/v1/mining/blocks/rewards/3y' \
+ '/api/v1/mining/blocks/rewards/all' \
do
curl -s "https://${hostname}${url}" >/dev/null