-
+
+
+
+
Avg Block Fee (24h)
+
+
+
+
+
+
+
+
Avg Block Fee (1m)
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss
index f4f4dcc77..21dd458b5 100644
--- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss
+++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss
@@ -57,7 +57,54 @@
.chart-widget {
width: 100%;
height: 100%;
- max-height: 270px;
+ max-height: 238px;
+}
+
+.block-fee-rates {
+ 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;
+ }
+ }
+ }
}
.formRadioGroup {
@@ -85,6 +132,13 @@
}
}
+.skeleton-loader {
+ width: 100%;
+ display: block;
+ max-width: 80px;
+ margin: 15px auto 3px;
+}
+
.disabled {
pointer-events: none;
opacity: 0.5;
diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts
index c4d061927..0283b2d00 100644
--- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts
+++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts
@@ -1,6 +1,6 @@
-import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
-import { EChartsOption } from '../../graphs/echarts';
-import { Observable, Subscription, combineLatest } from 'rxjs';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
+import { EChartsOption, graphic } from 'echarts';
+import { Observable, combineLatest, of } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
@@ -29,6 +29,7 @@ import { ActivatedRoute, Router } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockFeeRatesGraphComponent implements OnInit {
+ @Input() widget = false;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@@ -40,6 +41,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
renderer: 'svg',
};
+ hrStatsObservable$: Observable
;
statsObservable$: Observable;
isLoading = true;
formatNumber = formatNumber;
@@ -57,39 +59,61 @@ export class BlockFeeRatesGraphComponent implements OnInit {
private router: Router,
private zone: NgZone,
private route: ActivatedRoute,
+ private cd: ChangeDetectorRef,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
- this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
- this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
- this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
+ if (this.widget) {
+ this.miningWindowPreference = '1m';
+ } else {
+ this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
+ this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
+ this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
+ }
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
- this.route
- .fragment
- .subscribe((fragment) => {
- if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
- this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
- }
- });
+ if (!this.widget) {
+ this.route
+ .fragment
+ .subscribe((fragment) => {
+ if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
+ this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
+ }
+ });
+ }
+
+ this.hrStatsObservable$ = combineLatest([
+ this.apiService.getHistoricalBlockFeeRates$('24h'),
+ this.stateService.rateUnits$
+ ]).pipe(
+ map(([response, rateUnits]) => {
+ return {
+ blockCount: parseInt(response.headers.get('x-total-count'), 10),
+ avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0,
+ };
+ }),
+ share(),
+ );
this.statsObservable$ = combineLatest([
- this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
+ this.widget ? of(this.miningWindowPreference) : this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
this.stateService.rateUnits$
]).pipe(
switchMap(([timespan, rateUnits]) => {
- this.storageService.setValue('miningWindowPreference', timespan);
+ if (!this.widget) {
+ this.storageService.setValue('miningWindowPreference', timespan);
+ }
this.timespan = timespan;
this.isLoading = true;
return this.apiService.getHistoricalBlockFeeRates$(timespan)
.pipe(
tap((response) => {
// Group by percentile
- const seriesData = {
+ const seriesData = this.widget ? { 'Median': [] } : {
'Min': [],
'10th': [],
'25th': [],
@@ -100,13 +124,17 @@ export class BlockFeeRatesGraphComponent implements OnInit {
};
for (const rate of response.body) {
const timestamp = rate.timestamp * 1000;
- seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
- seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
- seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
- seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
- seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
- seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
- seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
+ if (this.widget) {
+ seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
+ } else {
+ seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
+ seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
+ seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
+ seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
+ seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
+ seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
+ seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
+ }
}
// Prepare chart
@@ -135,15 +163,42 @@ export class BlockFeeRatesGraphComponent implements OnInit {
});
}
+ if (this.widget) {
+ let maResolution = 30;
+ const medianMa = [];
+ for (let i = maResolution - 1; i < seriesData['Median'].length; ++i) {
+ let avg = 0;
+ for (let y = maResolution - 1; y >= 0; --y) {
+ avg += seriesData['Median'][i - y][1];
+ }
+ avg /= maResolution;
+ medianMa.push([seriesData['Median'][i][0], avg, seriesData['Median'][i][2]]);
+ }
+ series.push({
+ zlevel: 1,
+ name: 'Moving average',
+ data: medianMa,
+ type: 'line',
+ showSymbol: false,
+ symbol: 'none',
+ lineStyle: {
+ width: 3,
+ }
+ });
+ }
+
this.prepareChartOptions({
legends: legends,
series: series
}, rateUnits === 'wu');
+
this.isLoading = false;
+ this.cd.markForCheck();
}),
map((response) => {
return {
blockCount: parseInt(response.headers.get('x-total-count'), 10),
+ avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0,
};
}),
);
@@ -154,13 +209,19 @@ export class BlockFeeRatesGraphComponent implements OnInit {
prepareChartOptions(data, weightMode) {
this.chartOptions = {
- color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
+ color: this.widget ? ['#6b6b6b', 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' }
+ ])] : ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
animation: false,
grid: {
right: this.right,
left: this.left,
- bottom: 80,
- top: this.isMobile() ? 10 : 50,
+ bottom: this.widget ? 30 : 80,
+ top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
},
tooltip: {
show: !this.isMobile(),
@@ -184,9 +245,9 @@ export class BlockFeeRatesGraphComponent implements OnInit {
for (const rate of data.reverse()) {
if (weightMode) {
- tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1] / 4} sats/WU
`;
+ tooltip += `${rate.marker} ${rate.seriesName}: ${(rate.data[1] / 4).toFixed(2)} sats/WU
`;
} else {
- tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte
`;
+ tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1].toFixed(2)} sats/vByte
`;
}
}
@@ -201,7 +262,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
},
xAxis: data.series.length === 0 ? undefined :
{
- name: formatterXAxisLabel(this.locale, this.timespan),
+ name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
@@ -218,7 +279,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
padding: [0, 5],
},
},
- legend: (data.series.length === 0) ? undefined : {
+ legend: (this.widget || data.series.length === 0) ? undefined : {
padding: [10, 75],
data: data.legends,
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
@@ -256,7 +317,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined,
},
series: data.series,
- dataZoom: [{
+ dataZoom: this.widget ? null : [{
type: 'inside',
realtime: true,
zoomLock: true,
diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
index 68d2a1bf3..1fc173a2d 100644
--- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
@@ -4,7 +4,7 @@ import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
import TxView from './tx-view';
-import { Position } from './sprite-types';
+import { Color, Position } from './sprite-types';
import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@@ -27,6 +27,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false;
@Input() blockConversion: Price;
+ @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter();
@Output() readyEvent = new EventEmitter();
@@ -91,6 +92,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (changes.auditHighlighting) {
this.setHighlightingEnabled(this.auditHighlighting);
}
+ if (changes.overrideColor) {
+ this.scene.setColorFunction(this.overrideColors);
+ }
}
ngOnDestroy(): void {
@@ -228,7 +232,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
- highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
+ highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
+ colorFunction: this.overrideColors });
this.start();
}
}
diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts
index 2569a3bb2..77b7c2e05 100644
--- a/frontend/src/app/components/block-overview-graph/block-scene.ts
+++ b/frontend/src/app/components/block-overview-graph/block-scene.ts
@@ -1,12 +1,26 @@
import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/websocket.interface';
-import { Position, Square, ViewUpdateParams } from './sprite-types';
+import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
+import { feeLevels, mempoolFeeColors } from '../../app.constants';
+import { darken, desaturate, hexToColor } from './utils';
+
+const feeColors = mempoolFeeColors.map(hexToColor);
+const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
+const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
+const auditColors = {
+ censored: hexToColor('f344df'),
+ missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
+ added: hexToColor('0099ff'),
+ selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
+ accelerated: hexToColor('8F5FF6'),
+};
export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}};
vertexArray: FastVertexArray;
txs: { [key: string]: TxView };
+ getColor: ((tx: TxView) => Color) = defaultColorFunction;
orientation: string;
flip: boolean;
animationDuration: number = 1000;
@@ -26,11 +40,11 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
- constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
+ constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
- orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
+ orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
) {
- this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
+ this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@@ -63,6 +77,14 @@ export default class BlockScene {
}
}
+ setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
+ this.getColor = colorFunction;
+ this.dirty = true;
+ if (this.initialised && this.scene) {
+ this.updateColors(performance.now(), 50);
+ }
+ }
+
// Destroy the current layout and clean up graphics sprites without any exit animation
destroy(): void {
Object.values(this.txs).forEach(tx => tx.destroy());
@@ -86,7 +108,7 @@ export default class BlockScene {
this.applyTxUpdate(txView, {
display: {
position: txView.screenPosition,
- color: txView.getColor()
+ color: this.getColor(txView)
},
duration: 0
});
@@ -217,9 +239,9 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
- private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
+ private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
- orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
+ orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
@@ -228,6 +250,7 @@ export default class BlockScene {
this.flip = flip;
this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting;
+ this.getColor = colorFunction || defaultColorFunction;
this.scene = {
count: 0,
@@ -261,9 +284,23 @@ export default class BlockScene {
}
}
+ private updateColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration: number = 500): void {
+ if (tx.dirty || this.dirty) {
+ const txColor = this.getColor(tx);
+ this.applyTxUpdate(tx, {
+ display: {
+ color: txColor,
+ },
+ start: startTime,
+ delay,
+ duration: animate ? duration : 0,
+ });
+ }
+ }
+
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
if (!tx.initialised) {
- const txColor = tx.getColor();
+ const txColor = this.getColor(tx);
this.applyTxUpdate(tx, {
display: {
position: {
@@ -321,6 +358,15 @@ export default class BlockScene {
this.dirty = false;
}
+ private updateColors(startTime: number, delay: number = 50, animate: boolean = true, duration: number = 500): void {
+ const ids = this.getTxList();
+ startTime = startTime || performance.now();
+ for (const id of ids) {
+ this.updateColor(this.txs[id], startTime, delay, animate, duration);
+ }
+ this.dirty = false;
+ }
+
private remove(id: string, startTime: number, direction: string = 'left'): TxView | void {
const tx = this.txs[id];
if (tx) {
@@ -858,3 +904,48 @@ class BlockLayout {
function feeRateDescending(a: TxView, b: TxView) {
return b.feerate - a.feerate;
}
+
+function defaultColorFunction(tx: TxView): Color {
+ const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
+ const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
+ const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
+ // Normal mode
+ if (!tx.scene?.highlightingEnabled) {
+ if (tx.acc) {
+ return auditColors.accelerated;
+ } else {
+ return feeLevelColor;
+ }
+ return feeLevelColor;
+ }
+ // Block audit
+ switch(tx.status) {
+ case 'censored':
+ return auditColors.censored;
+ case 'missing':
+ case 'sigop':
+ case 'rbf':
+ return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
+ case 'fresh':
+ case 'freshcpfp':
+ return auditColors.missing;
+ case 'added':
+ return auditColors.added;
+ case 'selected':
+ return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
+ case 'accelerated':
+ return auditColors.accelerated;
+ case 'found':
+ if (tx.context === 'projected') {
+ return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
+ } else {
+ return feeLevelColor;
+ }
+ default:
+ if (tx.acc) {
+ return auditColors.accelerated;
+ } else {
+ return feeLevelColor;
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts
index db2c4f6ae..4e2d855e6 100644
--- a/frontend/src/app/components/block-overview-graph/tx-view.ts
+++ b/frontend/src/app/components/block-overview-graph/tx-view.ts
@@ -2,24 +2,13 @@ import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
-import { feeLevels, mempoolFeeColors } from '../../app.constants';
+import { hexToColor } from './utils';
import BlockScene from './block-scene';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
const defaultHighlightColor = hexToColor('800080');
-const feeColors = mempoolFeeColors.map(hexToColor);
-const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
-const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
-const auditColors = {
- censored: hexToColor('f344df'),
- missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
- added: hexToColor('0099ff'),
- selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
- accelerated: hexToColor('8F5FF6'),
-};
-
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
@@ -195,77 +184,4 @@ export default class TxView implements TransactionStripped {
this.dirty = false;
return performance.now() + hoverTransitionTime;
}
-
- getColor(): Color {
- const rate = this.fee / this.vsize; // color by simple single-tx fee rate
- const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
- const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
- // Normal mode
- if (!this.scene?.highlightingEnabled) {
- if (this.acc) {
- return auditColors.accelerated;
- } else {
- return feeLevelColor;
- }
- return feeLevelColor;
- }
- // Block audit
- switch(this.status) {
- case 'censored':
- return auditColors.censored;
- case 'missing':
- case 'sigop':
- case 'rbf':
- return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
- case 'fresh':
- case 'freshcpfp':
- return auditColors.missing;
- case 'added':
- return auditColors.added;
- case 'selected':
- return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
- case 'accelerated':
- return auditColors.accelerated;
- case 'found':
- if (this.context === 'projected') {
- return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
- } else {
- return feeLevelColor;
- }
- default:
- if (this.acc) {
- return auditColors.accelerated;
- } else {
- return feeLevelColor;
- }
- }
- }
-}
-
-function hexToColor(hex: string): Color {
- return {
- r: parseInt(hex.slice(0, 2), 16) / 255,
- g: parseInt(hex.slice(2, 4), 16) / 255,
- b: parseInt(hex.slice(4, 6), 16) / 255,
- a: 1
- };
-}
-
-function desaturate(color: Color, amount: number): Color {
- const gray = (color.r + color.g + color.b) / 6;
- return {
- r: color.r + ((gray - color.r) * amount),
- g: color.g + ((gray - color.g) * amount),
- b: color.b + ((gray - color.b) * amount),
- a: color.a,
- };
-}
-
-function darken(color: Color, amount: number): Color {
- return {
- r: color.r * amount,
- g: color.g * amount,
- b: color.b * amount,
- a: color.a,
- }
}
diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts
new file mode 100644
index 000000000..a0bb8e868
--- /dev/null
+++ b/frontend/src/app/components/block-overview-graph/utils.ts
@@ -0,0 +1,29 @@
+import { Color } from './sprite-types';
+
+export function hexToColor(hex: string): Color {
+ return {
+ r: parseInt(hex.slice(0, 2), 16) / 255,
+ g: parseInt(hex.slice(2, 4), 16) / 255,
+ b: parseInt(hex.slice(4, 6), 16) / 255,
+ a: hex.length > 6 ? parseInt(hex.slice(6, 8), 16) / 255 : 1
+ };
+}
+
+export function desaturate(color: Color, amount: number): Color {
+ const gray = (color.r + color.g + color.b) / 6;
+ return {
+ r: color.r + ((gray - color.r) * amount),
+ g: color.g + ((gray - color.g) * amount),
+ b: color.b + ((gray - color.b) * amount),
+ a: color.a,
+ };
+}
+
+export function darken(color: Color, amount: number): Color {
+ return {
+ r: color.r * amount,
+ g: color.g * amount,
+ b: color.b * amount,
+ a: color.a,
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts
index ba066d10a..0da8ca7b5 100644
--- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts
+++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts
@@ -79,7 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
enabledMiningInfoIfNeeded(url) {
- this.showMiningInfo = url.indexOf('/mining') !== -1;
+ this.showMiningInfo = url.includes('/mining') || url.includes('/acceleration');
this.cd.markForCheck(); // Need to update the view asap
}
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
index 503f2e38d..1e0cba48c 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
@@ -5,5 +5,6 @@
[blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'"
[flip]="true"
+ [overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"
>
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
index 226be5210..09eac989e 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
@@ -8,6 +8,8 @@ import { switchMap, filter } from 'rxjs/operators';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Router } from '@angular/router';
+import { Color } from '../block-overview-graph/sprite-types';
+import TxView from '../block-overview-graph/tx-view';
@Component({
selector: 'app-mempool-block-overview',
@@ -16,6 +18,7 @@ import { Router } from '@angular/router';
})
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
+ @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txPreviewEvent = new EventEmitter();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
index 0ddbbd4b7..61e62f642 100644
--- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
+++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
@@ -90,7 +90,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
) { }
enabledMiningInfoIfNeeded(url) {
- this.showMiningInfo = url.indexOf('/mining') !== -1;
+ this.showMiningInfo = url.includes('/mining') || url.includes('/acceleration');
this.cd.markForCheck(); // Need to update the view asap
}
diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts
index a2160977c..85905d1f1 100644
--- a/frontend/src/app/graphs/graphs.module.ts
+++ b/frontend/src/app/graphs/graphs.module.ts
@@ -3,6 +3,7 @@ import { NgxEchartsModule } from 'ngx-echarts';
import { GraphsRoutingModule } from './graphs.routing.module';
import { SharedModule } from '../shared/shared.module';
+import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component';
import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component';
@@ -19,6 +20,7 @@ import { PoolComponent } from '../components/pool/pool.component';
import { TelevisionComponent } from '../components/television/television.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
+import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component';
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
@@ -30,12 +32,14 @@ import { CommonModule } from '@angular/common';
MempoolBlockComponent,
MiningDashboardComponent,
+ AcceleratorDashboardComponent,
PoolComponent,
PoolRankingComponent,
TelevisionComponent,
StatisticsComponent,
GraphsComponent,
+ AccelerationFeesGraphComponent,
BlockFeesGraphComponent,
BlockRewardsGraphComponent,
BlockFeeRatesGraphComponent,
diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts
index 346bcf7f1..0f217eb6e 100644
--- a/frontend/src/app/graphs/graphs.routing.module.ts
+++ b/frontend/src/app/graphs/graphs.routing.module.ts
@@ -10,12 +10,15 @@ import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-ch
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
+import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component';
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
import { PoolComponent } from '../components/pool/pool.component';
import { StartComponent } from '../components/start/start.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
import { TelevisionComponent } from '../components/television/television.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
+import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
+import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
const routes: Routes = [
{
@@ -37,6 +40,22 @@ const routes: Routes = [
},
]
},
+ {
+ path: 'acceleration',
+ data: { networks: ['bitcoin'] },
+ component: StartComponent,
+ children: [
+ {
+ path: '',
+ component: AcceleratorDashboardComponent,
+ }
+ ]
+ },
+ {
+ path: 'acceleration-list',
+ data: { networks: ['bitcoin'] },
+ component: AccelerationsListComponent,
+ },
{
path: 'mempool-block/:id',
data: { networks: ['bitcoin', 'liquid'] },
@@ -93,6 +112,11 @@ const routes: Routes = [
data: { networks: ['bitcoin'] },
component: BlockSizesWeightsGraphComponent,
},
+ {
+ path: 'acceleration/fees',
+ data: { networks: ['bitcoin'] },
+ component: AccelerationFeesGraphComponent,
+ },
{
path: 'lightning',
data: { preload: true, networks: ['bitcoin'] },
diff --git a/frontend/src/app/shared/components/btc/btc.component.html b/frontend/src/app/shared/components/btc/btc.component.html
new file mode 100644
index 000000000..c13a8ff31
--- /dev/null
+++ b/frontend/src/app/shared/components/btc/btc.component.html
@@ -0,0 +1,8 @@
+{{ valueOverride }}
+{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ value | number }}
+
+ L-
+ tL-
+ t-
+ s-{{ unit }}
+
\ No newline at end of file
diff --git a/frontend/src/app/shared/components/btc/btc.component.scss b/frontend/src/app/shared/components/btc/btc.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/app/shared/components/btc/btc.component.ts b/frontend/src/app/shared/components/btc/btc.component.ts
new file mode 100644
index 000000000..4e62b07ff
--- /dev/null
+++ b/frontend/src/app/shared/components/btc/btc.component.ts
@@ -0,0 +1,44 @@
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { StateService } from '../../../services/state.service';
+
+@Component({
+ selector: 'app-btc',
+ templateUrl: './btc.component.html',
+ styleUrls: ['./btc.component.scss']
+})
+export class BtcComponent implements OnInit, OnChanges {
+ @Input() satoshis: number;
+ @Input() addPlus = false;
+ @Input() valueOverride: string | undefined = undefined;
+
+ value: number;
+ unit: string;
+
+ network = '';
+ stateSubscription: Subscription;
+
+ constructor(
+ private stateService: StateService,
+ ) { }
+
+ ngOnInit() {
+ this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
+ }
+
+ ngOnDestroy() {
+ if (this.stateSubscription) {
+ this.stateSubscription.unsubscribe();
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (this.satoshis >= 1_000_000) {
+ this.value = (this.satoshis / 100_000_000);
+ this.unit = 'BTC'
+ } else {
+ this.value = Math.round(this.satoshis);
+ this.unit = 'sats'
+ }
+ }
+}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 82327c561..52123f995 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -73,6 +73,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
import { ChangeComponent } from '../components/change/change.component';
import { SatsComponent } from './components/sats/sats.component';
+import { BtcComponent } from './components/btc/btc.component';
import { FeeRateComponent } from './components/fee-rate/fee-rate.component';
import { TruncateComponent } from './components/truncate/truncate.component';
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
@@ -85,6 +86,9 @@ import { GlobalFooterComponent } from './components/global-footer/global-footer.
import { AcceleratePreviewComponent } from '../components/accelerate-preview/accelerate-preview.component';
import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component';
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
+import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
+import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component';
+import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
@@ -167,6 +171,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
SvgImagesComponent,
ChangeComponent,
SatsComponent,
+ BtcComponent,
FeeRateComponent,
TruncateComponent,
SearchResultsComponent,
@@ -190,6 +195,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
OnlyVsizeDirective,
OnlyWeightDirective,
MempoolErrorComponent,
+ AccelerationsListComponent,
+ AccelerationStatsComponent,
+ PendingStatsComponent,
],
imports: [
CommonModule,
@@ -287,6 +295,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
SvgImagesComponent,
ChangeComponent,
SatsComponent,
+ BtcComponent,
FeeRateComponent,
TruncateComponent,
SearchResultsComponent,
@@ -300,6 +309,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AcceleratePreviewComponent,
AccelerateFeeGraphComponent,
MempoolErrorComponent,
+ AccelerationsListComponent,
+ AccelerationStatsComponent,
+ PendingStatsComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,