Merge branch 'master' into orangesurf/trademark-updates

This commit is contained in:
orangesurf 2023-12-11 16:43:45 +00:00 committed by GitHub
commit 9f6f210c38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1970 additions and 139 deletions

View file

@ -0,0 +1,42 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget">
<div *ngIf="!widget" class="card-header mb-0 mb-md-4">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="accelerator.acceleration-fees">Acceleration Fees</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
</div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 24H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3D
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1W
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 1M
</label>
</div>
</form>
</div>
<div *ngIf="widget">
<div class="item">
<h5 class="card-title" i18n="acceleration.block-fees">Out-of-band Fees Per Block</h5>
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View file

@ -0,0 +1,74 @@
.card-header {
border-bottom: 0;
font-size: 18px;
@media (min-width: 465px) {
font-size: 20px;
}
@media (min-width: 992px) {
height: 40px;
}
}
.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 {
display: flex;
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}
}
.chart {
display: flex;
flex: 1;
width: 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: 290px;
}
h5 {
margin-bottom: 10px;
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}

View file

@ -0,0 +1,390 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable, Subscription, combineLatest } from 'rxjs';
import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { SeoService } from '../../../services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-acceleration-fees-graph',
templateUrl: './acceleration-fees-graph.component.html',
styleUrls: ['./acceleration-fees-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
@Input() widget: boolean = false;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() accelerations$: Observable<Acceleration[]>;
miningWindowPreference: string;
radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
hrStatsObservable$: Observable<any>;
statsObservable$: Observable<any>;
statsSubscription: Subscription;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
currency: string;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
private cd: ChangeDetectorRef,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
this.currency = 'USD';
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`);
this.isLoading = true;
if (this.widget) {
this.miningWindowPreference = '1m';
this.timespan = this.miningWindowPreference;
this.statsObservable$ = combineLatest([
(this.accelerations$ || this.apiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
]).pipe(
tap(([accelerations, blockFeesResponse]) => {
this.prepareChartOptions(accelerations, blockFeesResponse.body);
}),
map(([accelerations, blockFeesResponse]) => {
return {
avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length
};
}),
);
} else {
this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
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'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
this.statsObservable$ = combineLatest([
this.radioGroupForm.get('dateSpan').valueChanges.pipe(
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
this.isLoading = true;
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
return this.apiService.getAccelerationHistory$({});
})
),
this.radioGroupForm.get('dateSpan').valueChanges.pipe(
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
return this.apiService.getHistoricalBlockFees$(timespan);
})
)
]).pipe(
tap(([accelerations, blockFeesResponse]) => {
this.prepareChartOptions(accelerations, blockFeesResponse.body);
})
);
}
this.statsSubscription = this.statsObservable$.subscribe(() => {
this.isLoading = false;
this.cd.markForCheck();
});
}
prepareChartOptions(accelerations, blockFees) {
let title: object;
const blockAccelerations = {};
for (const acceleration of accelerations) {
if (acceleration.status === 'completed') {
if (!blockAccelerations[acceleration.blockHeight]) {
blockAccelerations[acceleration.blockHeight] = [];
}
blockAccelerations[acceleration.blockHeight].push(acceleration);
}
}
let last = null;
let minValue = Infinity;
let maxValue = 0;
const data = [];
for (const val of blockFees) {
if (last == null) {
last = val.avgHeight;
}
let totalFeeDelta = 0;
let totalFeePaid = 0;
let totalCount = 0;
let blockCount = 0;
while (last <= val.avgHeight) {
blockCount++;
totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0);
totalCount += (blockAccelerations[last] || []).length;
last++;
}
minValue = Math.min(minValue, val.avgFees);
maxValue = Math.max(maxValue, val.avgFees);
data.push({
...val,
feeDelta: totalFeeDelta,
avgFeePaid: (totalFeePaid / blockCount),
accelerations: totalCount / blockCount,
});
}
this.chartOptions = {
title: title,
color: [
'#8F5FF6',
'#6b6b6b',
],
animation: false,
grid: {
right: this.right,
left: this.left,
bottom: this.widget ? 30 : 80,
top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
},
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: 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>`;
for (const tick of data.reverse()) {
if (tick.data[1] >= 1_000_000) {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100_000_000, this.locale, '1.0-3')} BTC<br>`;
} else {
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats<br>`;
}
}
if (['24h', '3d'].includes(this.timespan)) {
tooltip += `<small>` + $localize`At block: ${data[0].data[2]}` + `</small>`;
} else {
tooltip += `<small>` + $localize`Around block: ${data[0].data[2]}` + `</small>`;
}
return tooltip;
}.bind(this)
},
xAxis: data.length === 0 ? undefined :
{
name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
align: 'center',
fontSize: 11,
lineHeight: 12,
hideOverlap: true,
padding: [0, 5],
},
},
legend: {
data: [
{
name: 'In-band fees per block',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: 'Out-of-band fees per block',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
],
selected: {
'In-band fees per block': false,
'Out-of-band fees per block': true,
},
show: !this.widget,
},
yAxis: data.length === 0 ? undefined : [
{
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
if (val >= 100_000) {
return `${(val / 100_000_000).toFixed(3)} BTC`;
} else {
return `${val} sats`;
}
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
},
{
type: 'value',
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return `${val}`;
}.bind(this)
},
splitLine: {
show: false,
},
},
],
series: data.length === 0 ? undefined : [
{
legendHoverLink: false,
zlevel: 1,
name: 'Out-of-band fees per block',
data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
stack: 'Total',
type: 'bar',
barWidth: '100%',
large: true,
},
{
legendHoverLink: false,
zlevel: 0,
name: 'In-band fees per block',
data: data.map(block => [block.timestamp * 1000, block.avgFees, block.avgHeight]),
stack: 'Total',
type: 'bar',
barWidth: '100%',
large: true,
},
],
dataZoom: (this.widget || data.length === 0 )? undefined : [{
type: 'inside',
realtime: true,
zoomLock: true,
maxSpan: 100,
minSpan: 5,
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,
}
},
}],
visualMap: {
type: 'continuous',
min: minValue,
max: maxValue,
dimension: 1,
seriesIndex: 1,
show: false,
inRange: {
color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
}
},
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `acceleration-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
ngOnDestroy(): void {
if (this.statsSubscription) {
this.statsSubscription.unsubscribe();
}
}
}

View file

@ -0,0 +1,53 @@
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
<div class="stats-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<div class="card-text">
<div>{{ stats.count }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5>
<div class="card-text">
<div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
<span class="fiat">
<app-fiat [value]="stats.totalFeesPaid"></app-fiat>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
<div class="card-text">
<div>{{ stats.successRate.toFixed(2) }} %</div>
<div class="symbol" i18n="accelerator.mined-next-block">mined</div>
</div>
</div>
</div>
</div>
<ng-template #loading>
<div class="stats-container loading-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.out-of-band-fees">Out-of-band Fees</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View file

@ -0,0 +1,88 @@
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
display: inline-flex;
}
.green-color {
display: block;
}
}
.stats-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}

View file

@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
import { Acceleration } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-acceleration-stats',
templateUrl: './acceleration-stats.component.html',
styleUrls: ['./acceleration-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationStatsComponent implements OnInit {
@Input() timespan: '24h' | '1w' | '1m' = '24h';
@Input() accelerations$: Observable<Acceleration[]>;
public accelerationStats$: Observable<any>;
constructor(
private apiService: ApiService,
private stateService: StateService,
) { }
ngOnInit(): void {
this.accelerationStats$ = this.accelerations$.pipe(
switchMap(accelerations => {
let totalFeesPaid = 0;
let totalSucceeded = 0;
let totalCanceled = 0;
for (const acceleration of accelerations) {
if (acceleration.status === 'completed') {
totalSucceeded++;
totalFeesPaid += acceleration.feePaid || 0;
} else if (acceleration.status === 'failed') {
totalCanceled++;
}
}
return of({
count: totalSucceeded,
totalFeesPaid,
successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0,
});
})
);
}
}

View file

@ -0,0 +1,91 @@
<div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
<ng-container *ngIf="pending">
<th class="fee-rate text-right" i18n="transaction.fee|Transaction fee">Fee Rate</th>
<th class="bid text-right" i18n="transaction.fee|Transaction fee">Acceleration Bid</th>
<th class="time text-right" i18n="accelerator.block">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.fee|Transaction fee">Out-of-band Fee</th>
<th class="block text-right" i18n="accelerator.block">Block</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
</ng-container>
</thead>
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
</a>
</td>
<ng-container *ngIf="pending">
<td class="fee-rate text-right">
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
</td>
<td class="bid text-right">
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
</td>
</ng-container>
<ng-container *ngIf="!pending">
<td *ngIf="acceleration.feePaid" class="fee text-right">
{{ (acceleration.feePaid) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td *ngIf="!acceleration.feePaid" class="fee text-right">
~
</td>
<td class="block text-right">
<a [routerLink]="['/block' | relativeUrl, acceleration.blockHeight]">{{ acceleration.blockHeight }}</a>
</td>
<td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="transaction.rbf.mined">Pending</span>
<span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
</td>
</ng-container>
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="txid text-left">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="fee-delta text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="status text-right">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
<ng-template [ngIf]="!widget">
<div class="clearfix"></div>
<br>
</ng-template>
</div>
<ng-template #noData>
<div class="no-data">
<span *ngIf="pending" i18n="accelerations.no-accelerations">There are no active accelerations</span>
<span *ngIf="!pending" i18n="accelerations.no-accelerations">There are no recent accelerations</span>
</div>
</ng-template>
</div>

View file

@ -0,0 +1,125 @@
.spinner-border {
height: 25px;
width: 25px;
margin-top: 13px;
}
.container-xl {
max-width: 1400px;
}
.container-xl.widget {
padding-left: 0px;
padding-bottom: 0px;
}
.container-xl.legacy {
max-width: 1140px;
}
.container {
max-width: 100%;
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.8rem !important;
.difference {
margin-left: 0.5em;
&.positive {
color: rgb(66, 183, 71);
}
&.negative {
color: rgb(183, 66, 66);
}
}
}
.clear-link {
color: white;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.progress {
background-color: #2d3348;
}
.txid {
width: 25%;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
display: none;
}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
}
.fee {
width: 35%;
}
.block {
width: 20%;
}
.bid {
width: 30%;
}
.time {
width: 25%;
}
.status {
width: 20%
}
/* Tooltip text */
.tooltip-custom {
position: relative;
}
.tooltip-custom .tooltiptext {
visibility: hidden;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
top: -40px;
left: 0;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip-custom:hover .tooltiptext {
visibility: visible;
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 50vw;
text-align: left;
}
.no-data {
color: rgba(255, 255, 255, 0.4);
display: flex;
height: 280px;
width: 100%;
flex-direction: row;
align-items: center;
justify-content: center;
}

View file

@ -0,0 +1,71 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({
selector: 'app-accelerations-list',
templateUrl: './accelerations-list.component.html',
styleUrls: ['./accelerations-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationsListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() pending: boolean = false;
@Input() accelerations$: Observable<Acceleration[]>;
accelerationList$: Observable<Acceleration[]> = undefined;
isLoading = true;
paginationMaxSize: number;
page = 1;
lastPage = 1;
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
constructor(
private apiService: ApiService,
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
) {
}
ngOnInit(): void {
if (!this.widget) {
this.websocketService.want(['blocks']);
}
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.apiService.getAccelerations$() : this.apiService.getAccelerationHistory$({ timeframe: '1m' }));
this.accelerationList$ = accelerationObservable$.pipe(
switchMap(accelerations => {
if (this.pending) {
for (const acceleration of accelerations) {
acceleration.status = acceleration.status || 'accelerating';
}
}
if (this.widget) {
return of(accelerations.slice(-6).reverse());
} else {
return of(accelerations.reverse());
}
}),
catchError((err) => {
this.isLoading = false;
return of([]);
}),
tap(() => {
this.isLoading = false;
})
);
}
trackByBlock(index: number, block: BlockExtended): number {
return block.height;
}
}

View file

@ -0,0 +1,93 @@
<app-indexing-progress></app-indexing-progress>
<div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2">
<!-- pending stats -->
<div class="col">
<div class="main-title">
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active accelerations</span>
</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
<app-pending-stats [accelerations$]="pendingAccelerations$"></app-pending-stats>
</div>
</div>
</div>
</div>
<!-- 1m stats -->
<div class="col">
<div class="main-title">
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>&nbsp;
<span style="font-size: xx-small" i18n="mining.144-blocks">(1 month)</span>
</div>
<div class="card-wrapper">
<div class="card">
<div class="card-body more-padding">
<app-acceleration-stats timespan="1m" [accelerations$]="minedAccelerations$"></app-acceleration-stats>
</div>
</div>
</div>
</div>
<!-- Next block visualization -->
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<div class="mempool-block-wrapper">
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
</div>
</div>
</div>
</div>
<!-- acceleration fees graph -->
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<app-acceleration-fees-graph [attr.data-cy]="'acceleration-fees'" [widget]=true [accelerations$]="accelerations$"></app-acceleration-fees-graph>
<div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<!-- Avg block fees graph -->
<!-- <div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph>
<div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div> -->
<!-- Recent Accelerations List -->
<div class="col">
<div class="card list-card">
<div class="card-body">
<div class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Active Accelerations</h5>
</div>
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list>
</div>
</div>
</div>
<!-- Confirmed List -->
<div class="col">
<div class="card list-card">
<div class="card-body">
<a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<app-accelerations-list [attr.data-cy]="'recent-accelerations'" [widget]=true [accelerations$]="minedAccelerations$"></app-accelerations-list>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,148 @@
.dashboard-container {
text-align: center;
margin-top: 0.5rem;
.col {
margin-bottom: 1.5rem;
}
}
.card {
background-color: #1d1f31;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-title > a {
color: #4a68b9;
}
.card-body.pool-ranking {
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
}
.card-text {
font-size: 22px;
}
#blockchain-container {
position: relative;
overflow-x: scroll;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
#blockchain-container::-webkit-scrollbar {
display: none;
}
.fade-border {
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.more-padding {
padding: 24px 20px !important;
}
.card-wrapper {
.card {
height: auto !important;
}
.card-body {
display: flex;
flex: inherit;
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
}
}
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
.card-text {
font-size: 22px;
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
margin-bottom: 10px;
text-decoration: none;
color: inherit;
}
.lastest-blocks-table {
width: 100%;
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.8rem !important;
}
.table-cell-height {
width: 25%;
}
.table-cell-fee {
width: 25%;
text-align: right;
}
.table-cell-pool {
text-align: left;
width: 30%;
@media (max-width: 875px) {
display: none;
}
.pool-name {
margin-left: 1em;
}
}
.table-cell-acceleration-count {
text-align: right;
width: 20%;
}
}
.card {
height: 385px;
}
.list-card {
height: 410px;
}
.mempool-block-wrapper {
max-height: 380px;
max-width: 380px;
margin: auto;
}

View file

@ -0,0 +1,122 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { Observable, Subject, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, mempoolFeeColors } from '../../../app.constants';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
interface AccelerationBlock extends BlockExtended {
accelerationCount: number,
}
@Component({
selector: 'app-accelerator-dashboard',
templateUrl: './accelerator-dashboard.component.html',
styleUrls: ['./accelerator-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AcceleratorDashboardComponent implements OnInit {
blocks$: Observable<AccelerationBlock[]>;
accelerations$: Observable<Acceleration[]>;
pendingAccelerations$: Observable<Acceleration[]>;
minedAccelerations$: Observable<Acceleration[]>;
loadingBlocks: boolean = true;
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
private apiService: ApiService,
private stateService: StateService,
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.pendingAccelerations$ = interval(30000).pipe(
startWith(true),
switchMap(() => {
return this.apiService.getAccelerations$();
}),
catchError((e) => {
return of([]);
}),
share(),
);
this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap((chainTip) => {
return this.apiService.getAccelerationHistory$({ timeframe: '1m' });
}),
catchError((e) => {
return of([]);
}),
share(),
);
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status))
})
);
this.blocks$ = combineLatest([
this.accelerations$,
this.stateService.blocks$.pipe(
switchMap((blocks) => {
if (this.stateService.env.MINING_DASHBOARD === true) {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
}
return of(blocks as AccelerationBlock[]);
}),
tap(() => {
this.loadingBlocks = false;
})
)
]).pipe(
switchMap(([accelerations, blocks]) => {
const blockMap = {};
for (const block of blocks) {
blockMap[block.id] = block;
}
const accelerationsByBlock: { [ hash: string ]: Acceleration[] } = {};
for (const acceleration of accelerations) {
if (['mined', 'completed'].includes(acceleration.status) && acceleration.pools.includes(blockMap[acceleration.blockHash]?.extras.pool.id)) {
if (!accelerationsByBlock[acceleration.blockHash]) {
accelerationsByBlock[acceleration.blockHash] = [];
}
accelerationsByBlock[acceleration.blockHash].push(acceleration);
}
}
return of(blocks.slice(0, 6).map(block => {
block.accelerationCount = (accelerationsByBlock[block.id] || []).length;
return block;
}));
})
);
}
getAcceleratorColor(tx: TxView): Color {
if (tx.status === 'accelerated' || tx.acc) {
return acceleratedColor;
} else {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1];
}
}
}

View file

@ -0,0 +1,53 @@
<div class="stats-wrapper" *ngIf="accelerationStats$ | async as stats; else loading">
<div class="stats-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<div class="card-text">
<div>{{ stats.count }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5>
<div class="card-text">
<div>{{ stats.avgFeeDelta / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
<span class="fiat">
<app-fiat [value]="stats.avgFeeDelta"></app-fiat>
</span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
<div class="card-text">
<div [innerHTML]="'&lrm;' + (stats.totalVsize * 4 | vbytes: 2)"></div>
<div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div>
</div>
</div>
</div>
</div>
<ng-template #loading>
<div class="stats-container loading-container">
<div class="item">
<h5 class="card-title" i18n="address.transactions">Transactions</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.average-max-bid">Avg Max Bid</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>

View file

@ -0,0 +1,88 @@
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-text {
font-size: 22px;
span {
font-size: 11px;
position: relative;
top: -2px;
display: inline-flex;
}
.green-color {
display: block;
}
}
.stats-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.loading-container{
min-height: 76px;
}
.card-text {
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}

View file

@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { Acceleration } from '../../../interfaces/node-api.interface';
@Component({
selector: 'app-pending-stats',
templateUrl: './pending-stats.component.html',
styleUrls: ['./pending-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PendingStatsComponent implements OnInit {
@Input() accelerations$: Observable<Acceleration[]>;
public accelerationStats$: Observable<any>;
constructor(
private apiService: ApiService,
) { }
ngOnInit(): void {
this.accelerationStats$ = (this.accelerations$ || this.apiService.getAccelerations$()).pipe(
switchMap(accelerations => {
let totalAccelerations = 0;
let totalFeeDelta = 0;
let totalVsize = 0;
for (const acceleration of accelerations) {
totalAccelerations++;
totalFeeDelta += acceleration.feeDelta || 0;
totalVsize += acceleration.effectiveVsize || 0;
}
return of({
count: totalAccelerations,
totalFeeDelta,
avgFeeDelta: totalAccelerations ? totalFeeDelta / totalAccelerations : 0,
totalVsize,
});
})
);
}
}

View file

@ -1,13 +1,13 @@
<app-indexing-progress></app-indexing-progress>
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<div [class.full-container]="!widget">
<div *ngIf="!widget" class="card-header mb-0 mb-md-4">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
</div>
</div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
@ -45,11 +45,45 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div *ngIf="widget">
<div class="block-fee-rates">
<div class="item" *ngIf="(hrStatsObservable$ | async) as stats; else loadingHrStats">
<h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5>
<p class="card-text">
<app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate>
</p>
</div>
<div class="item" *ngIf="(statsObservable$ | async) as stats; else loadingStats">
<h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5>
<p class="card-text">
<app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate>
</p>
</div>
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>
</div>
<ng-template #loadingHrStats>
<div class="item">
<h5 class="card-title" i18n="mining.avg-block-fee-24h">Avg Block Fee (24h)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
</ng-template>
<ng-template #loadingStats>
<div class="item">
<h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
</ng-template>

View file

@ -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;

View file

@ -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<any>;
statsObservable$: Observable<any>;
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<br>`;
tooltip += `${rate.marker} ${rate.seriesName}: ${(rate.data[1] / 4).toFixed(2)} sats/WU<br>`;
} else {
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1]} sats/vByte<br>`;
tooltip += `${rate.marker} ${rate.seriesName}: ${rate.data[1].toFixed(2)} sats/vByte<br>`;
}
}
@ -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,

View file

@ -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<string>();
@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();
}
}

View file

@ -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;
}
}
}

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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
}

View file

@ -78,7 +78,7 @@
<div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div *ngIf="!servicesEnabled" class="empty-sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>

View file

@ -241,7 +241,7 @@ main {
}
// empty sidenav
.sidenav {
.empty-sidenav {
z-index: 1;
background-color: transparent;
width: 0px;

View file

@ -5,5 +5,6 @@
[blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'"
[flip]="true"
[overrideColors]="overrideColors"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>

View file

@ -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<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;

View file

@ -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
}

View file

@ -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,

View file

@ -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'] },

View file

@ -0,0 +1,8 @@
<span *ngIf="valueOverride !== undefined">{{ valueOverride }}</span>
<span *ngIf="valueOverride === undefined">&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ value | number }} </span>
<span class="symbol">
<ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t-</ng-template>
<ng-template [ngIf]="network === 'signet'">s-</ng-template>{{ unit }}
</span>

View file

@ -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'
}
}
}

View file

@ -11,7 +11,7 @@
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template>
</p>
<div class="site-options d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
<div class="site-options float-right d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
<div class="selector">
<app-language-selector></app-language-selector>
</div>

View file

@ -63,7 +63,6 @@ footer .row.main .links .category:not(:first-child) {
}
footer .site-options {
float: right;
margin-top: -20px;
}

View file

@ -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,

View file

@ -996,6 +996,27 @@ th {
.btn-audit {
margin-left: .5em;
}
.sidenav {
@extend .sidenav;
margin-left: 0px !important;
margin-right: -250px;
}
.sidenav.open {
margin-right: 0px;
}
.profile_image_container {
@extend .profile_image_container;
margin-right: 0px !important;
margin-left: 15px;
}
#blockchain-container.with-menu {
width: calc(100% + 120px);
left: 120px;
}
}
.scriptmessage {