Add USD serie in block fee/reward charts

This commit is contained in:
nymkappa 2022-06-06 10:14:40 +02:00
parent 47ad5fffc8
commit 80b3b91a82
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
11 changed files with 275 additions and 75 deletions

View file

@ -17,6 +17,9 @@ import { prepareBlock } from '../utils/blocks-utils';
import BlocksRepository from '../repositories/BlocksRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import RatesRepository from '../repositories/RatesRepository';
import database from '../database';
import poolsParser from './pools-parser';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import mining from './mining/mining';
@ -150,6 +153,7 @@ class Blocks {
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
blockExtended.extras.usd = fiatConversion.getConversionRates().USD;
if (block.height === 0) {
blockExtended.extras.medianFee = 0; // 50th percentiles

View file

@ -31,7 +31,7 @@ class Mining {
*/
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFees(
this.getTimeRange(interval),
this.getTimeRangeForAmounts(interval),
Common.getSqlInterval(interval)
);
}
@ -41,7 +41,7 @@ class Mining {
*/
public async $getHistoricalBlockRewards(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockRewards(
this.getTimeRange(interval),
this.getTimeRangeForAmounts(interval),
Common.getSqlInterval(interval)
);
}
@ -462,6 +462,21 @@ class Mining {
return date;
}
private getTimeRangeForAmounts(interval: string | null): number {
switch (interval) {
case '3y': return 1296000;
case '2y': return 864000;
case '1y': return 432000;
case '6m': return 216000;
case '3m': return 108000;
case '1m': return 36000;
case '1w': return 8400;
case '3d': return 3600;
case '24h': return 1200;
default: return 3888000;
}
}
private getTimeRange(interval: string | null): number {
switch (interval) {
case '3y': return 43200; // 12h
@ -473,7 +488,7 @@ class Mining {
case '1w': return 300; // 5min
case '3d': return 1;
case '24h': return 1;
default: return 86400; // 24h
default: return 86400;
}
}
}

View file

@ -109,6 +109,7 @@ export interface BlockExtension {
avgFee?: number;
avgFeeRate?: number;
coinbaseRaw?: string;
usd?: number | null;
}
export interface BlockExtended extends IEsploraApi.Block {

View file

@ -256,7 +256,7 @@ class BlocksRepository {
const params: any[] = [];
let query = ` SELECT
height,
blocks.height,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
@ -274,8 +274,10 @@ class BlocksRepository {
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
avg_fee_rate,
IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd
FROM blocks
LEFT JOIN rates on rates.height = blocks.height
WHERE pool_id = ?`;
params.push(pool.id);
@ -308,7 +310,7 @@ class BlocksRepository {
public async $getBlockByHeight(height: number): Promise<object | null> {
try {
const [rows]: any[] = await DB.query(`SELECT
height,
blocks.height,
hash,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
@ -333,10 +335,12 @@ class BlocksRepository {
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
avg_fee_rate,
IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE height = ${height};
LEFT JOIN rates on rates.height = blocks.height
WHERE blocks.height = ${height};
`);
if (rows.length <= 0) {
@ -357,12 +361,14 @@ class BlocksRepository {
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
SELECT *, blocks.height, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
previous_block_hash as previousblockhash,
IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
LEFT JOIN rates on rates.height = blocks.height
WHERE hash = '${hash}';
`;
const [rows]: any[] = await DB.query(query);
@ -473,10 +479,12 @@ class BlocksRepository {
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(blocks.height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(fees) as INT) as avgFees
FROM blocks`;
CAST(AVG(fees) as INT) as avgFees,
IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd
FROM blocks
LEFT JOIN rates on rates.height = blocks.height`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
@ -498,10 +506,12 @@ class BlocksRepository {
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(blocks.height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(reward) as INT) as avgRewards
FROM blocks`;
CAST(AVG(reward) as INT) as avgRewards,
IFNULL(JSON_EXTRACT(rates.bisq_rates, '$.USD'), null) as usd
FROM blocks
LEFT JOIN rates on rates.height = blocks.height`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;

View file

@ -27,6 +27,7 @@ export function prepareBlock(block: any): BlockExtended {
name: block.pool_name,
slug: block.pool_slug,
} : undefined),
usd: block?.extras?.usd ?? block.usd ?? null,
}
};
}

View file

@ -13,6 +13,7 @@ import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
@ -37,6 +38,7 @@ import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe
StorageService,
LanguageService,
ShortenStringPipe,
FiatShortenerPipe,
CapAddressPipe,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],

View file

@ -180,8 +180,8 @@ export class BlockFeeRatesGraphComponent implements OnInit {
}
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
for (const pool of data.reverse()) {
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]} sats/vByte<br>`;
for (const rate of data.reverse()) {
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`;
}
if (['24h', '3d'].includes(this.timespan)) {

View file

@ -4,12 +4,13 @@ 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 { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe';
@Component({
selector: 'app-block-fees-graph',
@ -51,6 +52,7 @@ export class BlockFeesGraphComponent implements OnInit {
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
private fiatShortenerPipe: FiatShortenerPipe,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
@ -82,6 +84,7 @@ export class BlockFeesGraphComponent implements OnInit {
tap((response) => {
this.prepareChartOptions({
blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000, val.avgHeight]),
blockFeesUSD: response.body.filter(val => val.usd > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val.usd, val.avgHeight]),
});
this.isLoading = false;
}),
@ -98,16 +101,17 @@ export class BlockFeesGraphComponent implements OnInit {
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' }
new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00ACC1' },
{ offset: 1, color: '#0D47A1' },
]),
new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' },
]),
],
animation: false,
grid: {
top: 30,
bottom: 80,
@ -128,28 +132,52 @@ export class BlockFeesGraphComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}</b><br>`;
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.3-3')} BTC`;
tooltip += `<br>`;
formatter: function (data) {
if (data.length <= 0) {
return '';
}
let tooltip = `<b style="color: white; margin-left: 2px">
${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
if (['24h', '3d'].includes(this.timespan)) {
tooltip += `<small>` + $localize`At block: ${ticks[0].data[2]}` + `</small>`;
} else {
tooltip += `<small>` + $localize`Around block: ${ticks[0].data[2]}` + `</small>`;
for (const tick of data) {
if (tick.seriesIndex === 0) {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
} else if (tick.seriesIndex === 1) {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
}
}
tooltip += `<small>* On average around block ${data[0].data[2]}</small>`;
return tooltip;
}
}.bind(this)
},
xAxis: {
name: formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
xAxis: data.blockFees.length === 0 ? undefined :
{
type: 'time',
splitNumber: this.isMobile() ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
legend: {
data: [
{
name: 'Fees BTC',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'Fees USD',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
],
},
yAxis: [
{
@ -160,6 +188,9 @@ export class BlockFeesGraphComponent implements OnInit {
return `${val} BTC`;
}
},
max: (value) => {
return Math.floor(value.max * 2 * 10) / 10;
},
splitLine: {
lineStyle: {
type: 'dotted',
@ -168,18 +199,47 @@ export class BlockFeesGraphComponent implements OnInit {
}
},
},
{
type: 'value',
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return this.fiatShortenerPipe.transform(val);
}.bind(this)
},
splitLine: {
show: false,
},
},
],
series: [
{
legendHoverLink: false,
zlevel: 0,
name: $localize`:@@c20172223f84462032664d717d739297e5a9e2fe:Fees`,
showSymbol: false,
symbol: 'none',
yAxisIndex: 0,
name: 'Fees BTC',
data: data.blockFees,
type: 'line',
smooth: 0.25,
symbol: 'none',
areaStyle: {
opacity: 0.25,
},
},
{
legendHoverLink: false,
zlevel: 1,
yAxisIndex: 1,
name: 'Fees USD',
data: data.blockFeesUSD,
type: 'line',
smooth: 0.25,
symbol: 'none',
lineStyle: {
width: 2,
},
opacity: 0.75,
}
},
],
dataZoom: [{

View file

@ -4,12 +4,13 @@ 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 { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { MiningService } from 'src/app/services/mining.service';
import { StorageService } from 'src/app/services/storage.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe';
@Component({
selector: 'app-block-rewards-graph',
@ -51,6 +52,7 @@ export class BlockRewardsGraphComponent implements OnInit {
private miningService: MiningService,
private storageService: StorageService,
private route: ActivatedRoute,
private fiatShortenerPipe: FiatShortenerPipe,
) {
}
@ -80,6 +82,7 @@ export class BlockRewardsGraphComponent implements OnInit {
tap((response) => {
this.prepareChartOptions({
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000, val.avgHeight]),
blockRewardsUSD: response.body.filter(val => val.usd > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val.usd, val.avgHeight]),
});
this.isLoading = false;
}),
@ -95,15 +98,18 @@ export class BlockRewardsGraphComponent implements OnInit {
}
prepareChartOptions(data) {
const scaleFactor = 0.1;
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' }
new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00ACC1' },
{ offset: 1, color: '#0D47A1' },
]),
new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FDD835' },
{ offset: 1, color: '#FB8C00' },
]),
],
grid: {
@ -126,33 +132,55 @@ export class BlockRewardsGraphComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}</b><br>`;
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.3-3')} BTC`;
tooltip += `<br>`;
formatter: function (data) {
if (data.length <= 0) {
return '';
}
let tooltip = `<b style="color: white; margin-left: 2px">
${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}</b><br>`;
if (['24h', '3d'].includes(this.timespan)) {
tooltip += `<small>` + $localize`At block: ${ticks[0].data[2]}` + `</small>`;
} else {
tooltip += `<small>` + $localize`Around block: ${ticks[0].data[2]}` + `</small>`;
for (const tick of data) {
if (tick.seriesIndex === 0) {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
} else if (tick.seriesIndex === 1) {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
}
}
tooltip += `<small>* On average around block ${data[0].data[2]}</small>`;
return tooltip;
}
}.bind(this)
},
xAxis: {
name: formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
xAxis: data.blockRewards.length === 0 ? undefined :
{
type: 'time',
splitNumber: this.isMobile() ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
legend: {
data: [
{
name: 'Rewards BTC',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'Rewards USD',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
],
},
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)',
@ -160,6 +188,12 @@ export class BlockRewardsGraphComponent implements OnInit {
return `${val} BTC`;
}
},
min: (value) => {
return Math.round(value.min * (1.0 - scaleFactor) * 10) / 10;
},
max: (value) => {
return Math.round(value.max * (1.0 + scaleFactor) * 10) / 10;
},
splitLine: {
lineStyle: {
type: 'dotted',
@ -168,18 +202,53 @@ export class BlockRewardsGraphComponent implements OnInit {
}
},
},
{
min: (value) => {
return Math.round(value.min * (1.0 - scaleFactor) * 10) / 10;
},
max: (value) => {
return Math.round(value.max * (1.0 + scaleFactor) * 10) / 10;
},
type: 'value',
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return this.fiatShortenerPipe.transform(val);
}.bind(this)
},
splitLine: {
show: false,
},
},
],
series: [
{
legendHoverLink: false,
zlevel: 0,
name: $localize`:@@12f86e6747a5ad39e62d3480ddc472b1aeab5b76:Reward`,
showSymbol: false,
symbol: 'none',
yAxisIndex: 0,
name: 'Rewards BTC',
data: data.blockRewards,
type: 'line',
smooth: 0.25,
symbol: 'none',
areaStyle: {
opacity: 0.25,
},
},
{
legendHoverLink: false,
zlevel: 1,
yAxisIndex: 1,
name: 'Rewards USD',
data: data.blockRewardsUSD,
type: 'line',
smooth: 0.25,
symbol: 'none',
lineStyle: {
width: 2,
},
opacity: 0.75,
}
},
],
dataZoom: [{

View file

@ -351,6 +351,7 @@ export class HashrateChartComponent implements OnInit {
series: data.hashrates.length === 0 ? [] : [
{
zlevel: 0,
yAxisIndex: 0,
name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`,
showSymbol: false,
symbol: 'none',

View file

@ -0,0 +1,37 @@
import { formatCurrency, getCurrencySymbol } from '@angular/common';
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fiatShortener'
})
export class FiatShortenerPipe implements PipeTransform {
constructor(
@Inject(LOCALE_ID) public locale: string
) {}
transform(num: number, ...args: any[]): unknown {
const digits = args[0] || 1;
const unit = args[1] || undefined;
if (num < 1000) {
return num.toFixed(digits);
}
const lookup = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
{ value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'G' },
{ value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' }
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup.slice().reverse().find((item) => num >= item.value);
let result = item ? (num / item.value).toFixed(digits).replace(rx, '$1') : '0';
result = formatCurrency(parseInt(result, 10), this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0');
return result + item.symbol;
}
}