Merge branch 'master' into mononaut/wallet-balance
This commit is contained in:
@@ -416,7 +416,7 @@
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<p>
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="https://mempool.space/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
</p>
|
||||
<div class="title">
|
||||
Trademark Notice<br>
|
||||
@@ -429,10 +429,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>mempool.space fee</small></i>
|
||||
<i><small>Accelerator Service Fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
@@ -141,7 +141,7 @@
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
<i><small>Transaction Size Surcharge</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
<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>
|
||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" formControlName="dateSpan"> 3M
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
@@ -35,6 +36,7 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable, Subscription, combineLatest, fromEvent } from 'rxjs';
|
||||
import { map, max, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
@@ -11,7 +11,6 @@ import { MiningService } from '../../../services/mining.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { ApiService } from '../../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
@@ -29,7 +28,7 @@ import { ApiService } from '../../../services/api.service';
|
||||
})
|
||||
export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() height: number | string = '200';
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
@@ -55,7 +54,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
@@ -69,104 +67,56 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoading = true;
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1m';
|
||||
this.timespan = this.miningWindowPreference;
|
||||
|
||||
this.statsObservable$ = combineLatest([
|
||||
(this.accelerations$ || this.servicesApiService.getAccelerationHistory$({ timeframe: this.miningWindowPreference })),
|
||||
this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
]).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 - acc.baseFee - acc.vsizeFee), 0) / accelerations.length
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.miningWindowPreference = '3m';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`);
|
||||
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.servicesApiService.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.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
|
||||
}
|
||||
this.statsSubscription = this.statsObservable$.subscribe(() => {
|
||||
this.isLoading = false;
|
||||
this.cd.markForCheck();
|
||||
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'].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) => {
|
||||
if (!this.widget) {
|
||||
this.storageService.setValue('miningWindowPreference', timespan);
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.timespan = timespan;
|
||||
return this.servicesApiService.getAggregatedAccelerationHistory$({timeframe: this.timespan});
|
||||
})
|
||||
),
|
||||
fromEvent(window, 'resize').pipe(startWith(null)),
|
||||
]).pipe(
|
||||
tap(([history]) => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(history);
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
|
||||
this.statsObservable$.subscribe();
|
||||
}
|
||||
|
||||
prepareChartOptions(accelerations, blockFees) {
|
||||
prepareChartOptions(data) {
|
||||
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 - acc.baseFee - acc.vsizeFee), 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,
|
||||
});
|
||||
if (data.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No accelerated transaction for this timeframe`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
@@ -177,11 +127,11 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
],
|
||||
animation: false,
|
||||
grid: {
|
||||
height: this.height,
|
||||
height: (this.widget && this.height) ? this.height - 30 : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 30 : 80,
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: this.widget ? 30 : 80,
|
||||
top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
@@ -197,29 +147,23 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
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>`;
|
||||
formatter: (ticks) => {
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[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 (ticks[0].data[1] > 10_000_000) {
|
||||
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC<br>`;
|
||||
} else {
|
||||
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].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>`;
|
||||
tooltip += `<small>` + $localize`At block: ${ticks[0].data[2]}` + `</small>`;
|
||||
} else {
|
||||
tooltip += `<small>` + $localize`Around block: ${data[0].data[2]}` + `</small>`;
|
||||
tooltip += `<small>` + $localize`Around block: ${ticks[0].data[2]}` + `</small>`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
}
|
||||
},
|
||||
xAxis: data.length === 0 ? undefined :
|
||||
{
|
||||
@@ -228,7 +172,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
type: 'category',
|
||||
type: 'time',
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
@@ -243,15 +187,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
legend: {
|
||||
data: [
|
||||
{
|
||||
name: 'In-band fees per block',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Total bid boost per block',
|
||||
name: 'Total bid boost',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -260,8 +196,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
],
|
||||
selected: {
|
||||
'In-band fees per block': false,
|
||||
'Total bid boost per block': true,
|
||||
'Total bid boost': true,
|
||||
},
|
||||
show: !this.widget,
|
||||
},
|
||||
@@ -304,21 +239,13 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
{
|
||||
legendHoverLink: false,
|
||||
zlevel: 1,
|
||||
name: 'Total bid boost per block',
|
||||
data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]),
|
||||
name: 'Total bid boost',
|
||||
data: data.map(h => {
|
||||
return [h.timestamp * 1000, h.sumBidBoost, h.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%',
|
||||
barWidth: '90%',
|
||||
large: true,
|
||||
},
|
||||
],
|
||||
@@ -347,17 +274,6 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
},
|
||||
}],
|
||||
visualMap: {
|
||||
type: 'continuous',
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
dimension: 1,
|
||||
seriesIndex: 1,
|
||||
show: false,
|
||||
inRange: {
|
||||
color: ['#F4511E7f', '#FB8C007f', '#FFB3007f', '#FDD8357f', '#7CB3427f'].reverse() // Gradient color range
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.count }}</div>
|
||||
<div>{{ stats.totalRequested }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
|
||||
<div>{{ stats.totalBidBoost / 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>
|
||||
<app-fiat [value]="stats.totalBidBoost"></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
|
||||
export type AccelerationStats = {
|
||||
totalRequested: number;
|
||||
totalBidBoost: number;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-stats',
|
||||
@@ -12,35 +15,13 @@ import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationStatsComponent implements OnInit {
|
||||
@Input() timespan: '24h' | '1w' | '1m' = '24h';
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
public accelerationStats$: Observable<any>;
|
||||
accelerationStats$: Observable<AccelerationStats>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
private servicesApiService: ServicesApiServices
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accelerationStats$ = this.accelerations$.pipe(
|
||||
switchMap(accelerations => {
|
||||
let totalFeesPaid = 0;
|
||||
let totalSucceeded = 0;
|
||||
let totalCanceled = 0;
|
||||
for (const acc of accelerations) {
|
||||
if (acc.status === 'completed') {
|
||||
totalSucceeded++;
|
||||
totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0;
|
||||
} else if (acc.status === 'failed') {
|
||||
totalCanceled++;
|
||||
}
|
||||
}
|
||||
return of({
|
||||
count: totalSucceeded,
|
||||
totalFeesPaid,
|
||||
successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0,
|
||||
});
|
||||
})
|
||||
);
|
||||
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget">
|
||||
<div class="container-lg widget-container" [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>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
|
||||
<th class="block text-right" i18n="accelerator.block">Block</th>
|
||||
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
|
||||
<th class="date text-right" i18n="" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
@@ -49,9 +50,13 @@
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||
<span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="acceleration.status === 'mined'" class="badge badge-info" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="acceleration.status === 'completed'" class="badge badge-success" i18n="">Completed</span>
|
||||
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
|
||||
</td>
|
||||
<td class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -75,6 +80,11 @@
|
||||
</ng-template>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="this.accelerationCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
|
||||
@@ -63,16 +63,28 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.txid {
|
||||
@media (max-width: 500px) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 15%;
|
||||
@media (max-width: 700px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fee, .block, .status {
|
||||
width: 15%;
|
||||
.status {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
width: 20%;
|
||||
.date {
|
||||
width: 20%;
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,23 +95,12 @@ tr, td, th {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 30%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fee-rate {
|
||||
width: 20%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 840px) and (min-width: 768px) {
|
||||
text-align: end !important;
|
||||
@media (max-width: 975px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
@@ -108,32 +109,31 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.bid {
|
||||
text-align: end !important;
|
||||
width: 30%;
|
||||
min-width: 150px;
|
||||
@media (max-width: 840px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 410px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 25%;
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1200px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fee {
|
||||
width: 30%;
|
||||
@media (max-width: 1060px) and (min-width: 768px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
text-align: start !important;
|
||||
}
|
||||
text-align: end !important;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 20%;
|
||||
@media (max-width: 1200px) and (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Observable, catchError, of, switchMap, tap } from 'rxjs';
|
||||
import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
@@ -21,9 +21,10 @@ export class AccelerationsListComponent implements OnInit {
|
||||
isLoading = true;
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
lastPage = 1;
|
||||
accelerationCount: number;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
skeletonLines: number[] = [];
|
||||
pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page);
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
@@ -40,34 +41,47 @@ export class AccelerationsListComponent implements OnInit {
|
||||
|
||||
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.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' }));
|
||||
this.accelerationList$ = accelerationObservable$.pipe(
|
||||
switchMap(accelerations => {
|
||||
if (this.pending) {
|
||||
for (const acceleration of accelerations) {
|
||||
acceleration.status = acceleration.status || 'accelerating';
|
||||
}
|
||||
}
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
|
||||
}
|
||||
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;
|
||||
|
||||
this.accelerationList$ = this.pageSubject.pipe(
|
||||
switchMap((page) => {
|
||||
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1y', page: page }));
|
||||
return accelerationObservable$.pipe(
|
||||
switchMap(response => {
|
||||
let accelerations = response;
|
||||
if (response.body) {
|
||||
accelerations = response.body;
|
||||
this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10);
|
||||
}
|
||||
if (this.pending) {
|
||||
for (const acceleration of accelerations) {
|
||||
acceleration.status = acceleration.status || 'accelerating';
|
||||
}
|
||||
}
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
|
||||
}
|
||||
if (this.widget) {
|
||||
return of(accelerations.slice(0, 6));
|
||||
} else {
|
||||
return of(accelerations);
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.isLoading = false;
|
||||
return of([]);
|
||||
}),
|
||||
tap(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.pageSubject.next(page);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>
|
||||
<span style="font-size: xx-small" i18n="mining.144-blocks">(1 month)</span>
|
||||
<span style="font-size: xx-small" i18n="mining.3-months">(3 months)</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>
|
||||
<app-acceleration-stats></app-acceleration-stats>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +59,6 @@
|
||||
[height]="graphHeight"
|
||||
[attr.data-cy]="'acceleration-fees'"
|
||||
[widget]=true
|
||||
[accelerations$]="accelerations$"
|
||||
></app-acceleration-fees-graph>
|
||||
</div>
|
||||
<div class="mt-1"><a [attr.data-cy]="'acceleration-fees-view-more'" [routerLink]="['/graphs/acceleration/fees' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
@@ -84,7 +83,7 @@
|
||||
<div class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
|
||||
</div>
|
||||
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list>
|
||||
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]=true [accelerations$]="pendingAccelerations$"></app-accelerations-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
this.accelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe(
|
||||
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '3m', page: 1, pageLength: 100}).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
@@ -71,7 +71,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
|
||||
this.minedAccelerations$ = this.accelerations$.pipe(
|
||||
map(accelerations => {
|
||||
return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status));
|
||||
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
|
||||
})
|
||||
);
|
||||
|
||||
@@ -128,11 +128,11 @@ export class AcceleratorDashboardComponent implements OnInit {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 330;
|
||||
this.graphHeight = 380;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 245;
|
||||
this.graphHeight = 300;
|
||||
} else {
|
||||
this.graphHeight = 210;
|
||||
this.graphHeight = 270;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-ty
|
||||
import { hexToColor } from './utils';
|
||||
import BlockScene from './block-scene';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { TransactionFlags } from '../../shared/filters.utils';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
@@ -58,7 +59,7 @@ export default class TxView implements TransactionStripped {
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
this.bigintFlags = tx.flags ? (BigInt(tx.flags) | (this.acc ? TransactionFlags.acceleration : 0n)): 0n;
|
||||
this.initialised = false;
|
||||
this.vertexArray = scene.vertexArray;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<app-fee-rate [fee]="feeRate"></app-fee-rate>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||
<tr *ngIf="hasEffectiveRate && effectiveRate != null">
|
||||
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||
<td>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
text-align: left;
|
||||
min-width: 320px;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
&.clickable {
|
||||
pointer-events: all;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStra
|
||||
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
|
||||
import { TransactionFlags } from '../../shared/filters.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-tooltip',
|
||||
@@ -22,6 +23,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
feeRate = 0;
|
||||
effectiveRate;
|
||||
acceleration;
|
||||
hasEffectiveRate: boolean = false;
|
||||
|
||||
tooltipPosition: Position = { x: 0, y: 0 };
|
||||
|
||||
@@ -55,6 +57,8 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
||||
this.feeRate = this.fee / this.vsize;
|
||||
this.effectiveRate = tx.rate;
|
||||
this.acceleration = tx.acc;
|
||||
this.hasEffectiveRate = Math.abs((this.fee / this.vsize) - this.effectiveRate) > 0.05
|
||||
|| (tx.bigintFlags && (tx.bigintFlags & (TransactionFlags.cpfp_child | TransactionFlags.cpfp_parent)) > 0n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0;
|
||||
opacity: 0.4;
|
||||
pointer-events : none;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,15 +46,13 @@
|
||||
<div class="item" *ngIf="showHalving">
|
||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
|
||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||
<span>{{ timeUntilHalving | date }}</span>
|
||||
<div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
|
||||
<app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
|
||||
</div>
|
||||
<ng-template #approxTime>
|
||||
<div class="symbol">
|
||||
<span>{{ timeUntilHalving | date }}</span>
|
||||
<app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
<div [ngClass]="{'widget': widget, 'extra-margin-right': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.extra-margin-right {
|
||||
@media (max-width: 380px) {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem;
|
||||
|
||||
@@ -31,7 +31,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.transaction {
|
||||
width: 20%;
|
||||
width: 65%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -57,7 +57,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.output {
|
||||
width: 20%;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
|
||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="stateService.env.ACCELERATOR">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="network.val === '' && stateService.env.ACCELERATOR">
|
||||
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()">
|
||||
<fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon>
|
||||
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[showFilters]="showFilters"
|
||||
[filterFlags]="filterFlags"
|
||||
[filterMode]="filterMode"
|
||||
[excludeFilters]="['nonstandard']"
|
||||
[overrideColors]="overrideColors"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
|
||||
@@ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
const i = pool.blockCount.toString();
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
||||
pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||
@@ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
const i = totalBlockOther.toString();
|
||||
if (this.miningWindowPreference === '24h') {
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
totalEstimatedHashrateOther.toString() + ' PH/s' +
|
||||
totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
|
||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
||||
} else {
|
||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="tomahawk container-xl dashboard-container">
|
||||
<div class="links">
|
||||
<span>Status</span>
|
||||
<a [routerLink]='"/network"'>Live</a>
|
||||
</div>
|
||||
<h1 class="dashboard-title">Node Status</h1>
|
||||
|
||||
<ng-container *ngIf="(hosts$ | async) as hosts">
|
||||
<div class="status-panel">
|
||||
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="rank"></th>
|
||||
<th class="flag"></th>
|
||||
<th class="host">Host</th>
|
||||
<th class="rtt only-small">RTT</th>
|
||||
<th class="rtt only-large">RTT</th>
|
||||
<th class="height">Height</th>
|
||||
</tr>
|
||||
<tr *ngFor="let host of hosts; let i = index; trackBy: trackByFn">
|
||||
<td class="rank">{{ i + 1 }}</td>
|
||||
<td class="flag">{{ host.active ? '⭐️' : host.flag }}</td>
|
||||
<td class="host">{{ host.link }}</td>
|
||||
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
|
||||
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < tip ? '🟧' : '✅')) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
.tomahawk {
|
||||
.links {
|
||||
float: right;
|
||||
text-align: right;
|
||||
margin-top: 1em;
|
||||
|
||||
a, span {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
max-width: 720px;
|
||||
margin: auto;
|
||||
margin-top: 2em;
|
||||
padding: 1em;
|
||||
background: #24273e;
|
||||
}
|
||||
|
||||
.status-table {
|
||||
width: 100%;
|
||||
|
||||
td, th {
|
||||
padding: 0.25em;
|
||||
|
||||
&.rank, &.flag {
|
||||
width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
&.only-small {
|
||||
display: table-cell;
|
||||
&.rtt {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
&.only-large {
|
||||
display: none;
|
||||
}
|
||||
&.height {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
&.host {
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
&.rank, &.flag {
|
||||
width: 32px;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 96px;
|
||||
}
|
||||
&.only-small {
|
||||
display: none;
|
||||
}
|
||||
&.only-large {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { Observable, Subject, map } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { HealthCheckHost } from '../../interfaces/websocket.interface';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-health',
|
||||
templateUrl: './server-health.component.html',
|
||||
styleUrls: ['./server-health.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServerHealthComponent implements OnInit {
|
||||
hosts$: Observable<HealthCheckHost[]>;
|
||||
tip$: Subject<number>;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
public sanitizer: DomSanitizer,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hosts$ = this.stateService.serverHealth$.pipe(
|
||||
map((hosts) => {
|
||||
const subpath = window.location.pathname.slice(0, -6);
|
||||
for (const host of hosts) {
|
||||
let statusUrl = '';
|
||||
let linkHost = '';
|
||||
if (host.socket) {
|
||||
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
|
||||
linkHost = window.location.hostname + subpath;
|
||||
} else {
|
||||
const hostUrl = new URL(host.host);
|
||||
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
|
||||
linkHost = hostUrl.hostname + subpath;
|
||||
}
|
||||
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
|
||||
host.link = linkHost;
|
||||
host.flag = this.parseFlag(host.host);
|
||||
}
|
||||
return hosts;
|
||||
})
|
||||
);
|
||||
this.tip$ = this.stateService.chainTip$;
|
||||
this.websocketService.want(['blocks', 'tomahawk']);
|
||||
}
|
||||
|
||||
trackByFn(index: number, host: HealthCheckHost): string {
|
||||
return host.host;
|
||||
}
|
||||
|
||||
private parseFlag(host: string): string {
|
||||
if (host.includes('.fra.')) {
|
||||
return '🇩🇪';
|
||||
} else if (host.includes('.tk7.')) {
|
||||
return '🇯🇵';
|
||||
} else if (host.includes('.fmt.')) {
|
||||
return '🇺🇸';
|
||||
} else if (host.includes('.va1.')) {
|
||||
return '🇺🇸';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="tomahawk">
|
||||
<div class="container-xl dashboard-container">
|
||||
<div class="links">
|
||||
<a [routerLink]='"/nodes"'>Status</a>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
<h1 class="dashboard-title">Live Network</h1>
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let host of hosts; trackBy: trackByFn">
|
||||
<h5 [id]="host.host" class="hostLink">
|
||||
<a [href]="'https://' + host.link">{{ host.link }}</a>
|
||||
</h5>
|
||||
<iframe class="mempoolStatus" [src]="host.statusPage"></iframe>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
.tomahawk {
|
||||
.links {
|
||||
float: right;
|
||||
text-align: right;
|
||||
margin-top: 1em;
|
||||
|
||||
a, span {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mempoolStatus {
|
||||
width: 100%;
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.hostLink {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, SecurityContext, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { Observable, Subject, Subscription, map, tap } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { HealthCheckHost } from '../../interfaces/websocket.interface';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-status',
|
||||
templateUrl: './server-status.component.html',
|
||||
styleUrls: ['./server-status.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ServerStatusComponent implements OnInit, OnDestroy {
|
||||
tip$: Subject<number>;
|
||||
hosts: HealthCheckHost[] = [];
|
||||
hostSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
public sanitizer: DomSanitizer,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hostSubscription = this.stateService.serverHealth$.pipe(
|
||||
map((hosts) => {
|
||||
const subpath = window.location.pathname.slice(0, -8);
|
||||
for (const host of hosts) {
|
||||
let statusUrl = '';
|
||||
let linkHost = '';
|
||||
if (host.socket) {
|
||||
statusUrl = 'https://' + window.location.hostname + subpath + '/status';
|
||||
linkHost = window.location.hostname + subpath;
|
||||
} else {
|
||||
const hostUrl = new URL(host.host);
|
||||
statusUrl = 'https://' + hostUrl.hostname + subpath + '/status';
|
||||
linkHost = hostUrl.hostname + subpath;
|
||||
}
|
||||
host.statusPage = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, statusUrl));
|
||||
host.link = linkHost;
|
||||
}
|
||||
return hosts;
|
||||
}),
|
||||
tap((hosts) => {
|
||||
if (this.hosts.length !== hosts.length) {
|
||||
this.hosts = hosts.sort((a,b) => {
|
||||
const aParts = (a.host?.split('.') || []).reverse();
|
||||
const bParts = (b.host?.split('.') || []).reverse();
|
||||
let i = 0;
|
||||
while (i < Math.max(aParts.length, bParts.length)) {
|
||||
if (aParts[i] && !bParts[i]) {
|
||||
return 1;
|
||||
} else if (bParts[i] && !aParts[i]) {
|
||||
return -1;
|
||||
} else if (aParts[i] !== bParts[i]) {
|
||||
return aParts[i].localeCompare(bParts[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
).subscribe();
|
||||
this.tip$ = this.stateService.chainTip$;
|
||||
this.websocketService.want(['blocks', 'tomahawk']);
|
||||
}
|
||||
|
||||
trackByFn(index: number, host: HealthCheckHost): string {
|
||||
return host.host;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.hosts = [];
|
||||
this.hostSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
import { Price, PriceService } from '../../services/price.service';
|
||||
import { isFeatureActive } from '../../bitcoin.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
@@ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private seoService: SeoService,
|
||||
private priceService: PriceService,
|
||||
private storageService: StorageService
|
||||
private storageService: StorageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
|
||||
this.enterpriseService.page();
|
||||
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => {
|
||||
@@ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (!this.txId) {
|
||||
return;
|
||||
}
|
||||
this.enterpriseService.goal(8);
|
||||
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
||||
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user