Merge branch 'master' into mononaut/mobile-chain-jumping
This commit is contained in:
@@ -72,22 +72,10 @@ export const chartColors = [
|
||||
];
|
||||
|
||||
export const poolsColor = {
|
||||
'foundryusa': '#D81B60',
|
||||
'antpool': '#8E24AA',
|
||||
'f2pool': '#5E35B1',
|
||||
'poolin': '#3949AB',
|
||||
'binancepool': '#1E88E5',
|
||||
'viabtc': '#039BE5',
|
||||
'btccom': '#00897B',
|
||||
'braiinspool': '#00ACC1',
|
||||
'sbicrypto': '#43A047',
|
||||
'marapool': '#7CB342',
|
||||
'luxor': '#C0CA33',
|
||||
'unknown': '#FDD835',
|
||||
'okkong': '#FFB300',
|
||||
}
|
||||
'unknown': '#9C9C9C',
|
||||
};
|
||||
|
||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
export interface Language {
|
||||
@@ -157,3 +145,41 @@ export const specialBlocks = {
|
||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||
}
|
||||
};
|
||||
|
||||
export const fiatCurrencies = {
|
||||
AUD: {
|
||||
name: 'Australian Dollar',
|
||||
code: 'AUD',
|
||||
indexed: true,
|
||||
},
|
||||
CAD: {
|
||||
name: 'Canadian Dollar',
|
||||
code: 'CAD',
|
||||
indexed: true,
|
||||
},
|
||||
CHF: {
|
||||
name: 'Swiss Franc',
|
||||
code: 'CHF',
|
||||
indexed: true,
|
||||
},
|
||||
EUR: {
|
||||
name: 'Euro',
|
||||
code: 'EUR',
|
||||
indexed: true,
|
||||
},
|
||||
GBP: {
|
||||
name: 'Pound Sterling',
|
||||
code: 'GBP',
|
||||
indexed: true,
|
||||
},
|
||||
JPY: {
|
||||
name: 'Japanese Yen',
|
||||
code: 'JPY',
|
||||
indexed: true,
|
||||
},
|
||||
USD: {
|
||||
name: 'US Dollar',
|
||||
code: 'USD',
|
||||
indexed: true,
|
||||
},
|
||||
};
|
||||
@@ -17,6 +17,7 @@ import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
@@ -34,6 +35,7 @@ const providers = [
|
||||
LanguageService,
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
FiatCurrencyPipe,
|
||||
CapAddressPipe,
|
||||
AppPreloadingStrategy,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
|
||||
.community-sponsor {
|
||||
img {
|
||||
width: 67px;
|
||||
height: 67px;
|
||||
width: 57px;
|
||||
height: 57px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="container-xl">
|
||||
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
||||
<div class="title-address">
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<div class="tx-link">
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<table class="table table-borderless table-striped address-table">
|
||||
<tbody>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
|
||||
@@ -91,3 +91,20 @@ h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.liquid-address {
|
||||
.address-table {
|
||||
table-layout: fixed;
|
||||
|
||||
tr td:first-child {
|
||||
width: 170px;
|
||||
}
|
||||
tr td:last-child {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
flex-grow: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
@@ -10,10 +10,12 @@ import { Observable, Subscription } from 'rxjs';
|
||||
})
|
||||
export class AmountComponent implements OnInit, OnDestroy {
|
||||
conversions$: Observable<any>;
|
||||
currency: string;
|
||||
viewFiat$: Observable<boolean>;
|
||||
network = '';
|
||||
|
||||
stateSubscription: Subscription;
|
||||
currencySubscription: Subscription;
|
||||
|
||||
@Input() satoshis: number;
|
||||
@Input() digitsInfo = '1.8-8';
|
||||
@@ -22,7 +24,13 @@ export class AmountComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||
@@ -34,6 +42,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
||||
if (this.stateSubscription) {
|
||||
this.stateSubscription.unsubscribe();
|
||||
}
|
||||
this.currencySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { fiatCurrencies } from '../../app.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-fees-graph',
|
||||
@@ -44,6 +47,9 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
@@ -51,11 +57,21 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private stateService: StateService,
|
||||
private route: ActivatedRoute,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
||||
this.currency = fiat;
|
||||
} else {
|
||||
this.currency = 'USD';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -84,7 +100,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
tap((response) => {
|
||||
this.prepareChartOptions({
|
||||
blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000, val.avgHeight]),
|
||||
blockFeesUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val.USD, val.avgHeight]),
|
||||
blockFeesFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val[this.currency], val.avgHeight]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
@@ -157,7 +173,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
if (tick.seriesIndex === 0) {
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
||||
} else if (tick.seriesIndex === 1) {
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency) }<br>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +200,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Fees USD',
|
||||
name: 'Fees ' + this.currency,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -216,7 +232,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return this.fiatShortenerPipe.transform(val);
|
||||
return this.fiatShortenerPipe.transform(val, null, this.currency);
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
@@ -243,8 +259,8 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
legendHoverLink: false,
|
||||
zlevel: 1,
|
||||
yAxisIndex: 1,
|
||||
name: 'Fees USD',
|
||||
data: data.blockFeesUSD,
|
||||
name: 'Fees ' + this.currency,
|
||||
data: data.blockFeesFiat,
|
||||
type: 'line',
|
||||
smooth: 0.25,
|
||||
symbol: 'none',
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { fiatCurrencies } from '../../app.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-rewards-graph',
|
||||
@@ -44,16 +47,28 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private miningService: MiningService,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
private route: ActivatedRoute,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
) {
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
||||
this.currency = fiat;
|
||||
} else {
|
||||
this.currency = 'USD';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -82,7 +97,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
tap((response) => {
|
||||
this.prepareChartOptions({
|
||||
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000, val.avgHeight]),
|
||||
blockRewardsUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val.USD, val.avgHeight]),
|
||||
blockRewardsFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val[this.currency], val.avgHeight]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
@@ -157,7 +172,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
if (tick.seriesIndex === 0) {
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
||||
} else if (tick.seriesIndex === 1) {
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency)}<br>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +199,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Rewards USD',
|
||||
name: 'Rewards ' + this.currency,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -228,7 +243,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return this.fiatShortenerPipe.transform(val);
|
||||
return this.fiatShortenerPipe.transform(val, null, this.currency);
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
@@ -251,8 +266,8 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
legendHoverLink: false,
|
||||
zlevel: 1,
|
||||
yAxisIndex: 1,
|
||||
name: 'Rewards USD',
|
||||
data: data.blockRewardsUSD,
|
||||
name: 'Rewards ' + this.currency,
|
||||
data: data.blockRewardsFiat,
|
||||
type: 'line',
|
||||
smooth: 0.25,
|
||||
symbol: 'none',
|
||||
|
||||
@@ -90,6 +90,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
this.prepareChartOptions({
|
||||
sizes: data.sizes.map(val => [val.timestamp * 1000, val.avgSize / 1000000, val.avgHeight]),
|
||||
weights: data.weights.map(val => [val.timestamp * 1000, val.avgWeight / 1000000, val.avgHeight]),
|
||||
sizePerWeight: data.weights.map((val, i) => [val.timestamp * 1000, data.sizes[i].avgSize / (val.avgWeight / 4), val.avgHeight]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
@@ -124,6 +125,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
color: [
|
||||
'#FDD835',
|
||||
'#D81B60',
|
||||
'#039BE5',
|
||||
],
|
||||
grid: {
|
||||
top: 30,
|
||||
@@ -153,6 +155,8 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')} MB`;
|
||||
} else if (tick.seriesIndex === 1) { // Weight
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')} MWU`;
|
||||
} else if (tick.seriesIndex === 2) { // Size per weight
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')} B/vB`;
|
||||
}
|
||||
tooltip += `<br>`;
|
||||
}
|
||||
@@ -192,10 +196,19 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: $localize`Size per weight`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
selected: JSON.parse(this.storageService.getValue('sizes_weights_legend')) ?? {
|
||||
'Size': true,
|
||||
'Weight': true,
|
||||
'Size per weight': true,
|
||||
}
|
||||
},
|
||||
yAxis: data.sizes.length === 0 ? undefined : [
|
||||
@@ -262,6 +275,18 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
}
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Size per weight`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.sizePerWeight,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
}
|
||||
}
|
||||
],
|
||||
dataZoom: [{
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="auditAvailable">
|
||||
<td i18n="latest-blocks.health">Health</td>
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td>
|
||||
<span
|
||||
class="health-badge badge"
|
||||
@@ -143,7 +143,7 @@
|
||||
</ng-template>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees</td>
|
||||
<td>
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span class="fiat">
|
||||
@@ -158,7 +158,7 @@
|
||||
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@@ -206,13 +206,13 @@
|
||||
<div class="box" *ngIf="!error && webGlEnabled && showAudit">
|
||||
<div class="nav nav-tabs" *ngIf="isMobile && showAudit">
|
||||
<a class="nav-link" [class.active]="mode === 'projected'"
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.projected">Projected</ng-container> <span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
|
||||
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container> <span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
|
||||
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.projected-block">Projected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
@@ -221,7 +221,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
@@ -231,7 +231,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||
<div [hidden]="!showDetails" id="details">
|
||||
<br>
|
||||
|
||||
@@ -34,9 +34,24 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.info-link {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-subtitle.actual a {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.block-subtitle.actual fa-icon {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 18px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
|
||||
@@ -57,7 +57,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
transactionsError: any = null;
|
||||
overviewError: any = null;
|
||||
webGlEnabled = true;
|
||||
indexingAvailable = false;
|
||||
auditSupported: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
|
||||
auditModeEnabled: boolean = !this.stateService.hideAudit.value;
|
||||
auditAvailable = true;
|
||||
showAudit: boolean;
|
||||
@@ -109,13 +109,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.timeLtr = !!ltr;
|
||||
});
|
||||
|
||||
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
|
||||
this.setAuditAvailable(this.indexingAvailable);
|
||||
this.setAuditAvailable(this.auditSupported);
|
||||
|
||||
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
|
||||
this.auditModeEnabled = !hide;
|
||||
this.showAudit = this.auditAvailable && this.auditModeEnabled;
|
||||
});
|
||||
if (this.auditSupported) {
|
||||
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
|
||||
this.auditModeEnabled = !hide;
|
||||
this.showAudit = this.auditAvailable && this.auditModeEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
this.txsLoadingStatus$ = this.route.paramMap
|
||||
.pipe(
|
||||
@@ -221,7 +222,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
setTimeout(() => {
|
||||
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
||||
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
||||
this.apiService.getBlockAudit$(block.previousblockhash);
|
||||
if (this.auditSupported) {
|
||||
this.apiService.getBlockAudit$(block.previousblockhash);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||
@@ -269,7 +272,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingOverview = false;
|
||||
});
|
||||
|
||||
if (!this.indexingAvailable) {
|
||||
if (!this.auditSupported) {
|
||||
this.overviewSubscription = block$.pipe(
|
||||
startWith(null),
|
||||
pairwise(),
|
||||
@@ -300,7 +303,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
if (this.auditSupported) {
|
||||
this.auditSubscription = block$.pipe(
|
||||
startWith(null),
|
||||
pairwise(),
|
||||
@@ -605,7 +608,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
setAuditAvailable(available: boolean): void {
|
||||
this.auditAvailable = available;
|
||||
this.showAudit = this.auditAvailable && this.auditModeEnabled;
|
||||
this.showAudit = this.auditAvailable && this.auditModeEnabled && this.auditSupported;
|
||||
}
|
||||
|
||||
toggleAuditMode(): void {
|
||||
@@ -613,6 +616,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
||||
if (!this.auditSupported) {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
switch (this.stateService.network) {
|
||||
case 'testnet':
|
||||
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||
|
||||
@@ -1,64 +1,86 @@
|
||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [style.left]="static ? (offset || 0) + 'px' : null" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
|
||||
[style.left]="static ? (offset || 0) + 'px' : null"
|
||||
*ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
|
||||
<ng-container *ngIf="block && !block.loading && !block.placeholder; else placeholderBlock">
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
||||
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
|
||||
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
||||
[class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
|
||||
}}</a>
|
||||
</div>
|
||||
<div class="block-body">
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees">
|
||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
|
||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="block?.extras?.feeRange">
|
||||
{{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
*ngIf="block?.extras?.feeRange">
|
||||
{{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{
|
||||
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
|
||||
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="!block?.extras?.feeRange">
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
|
||||
*ngIf="!block?.extras?.feeRange">
|
||||
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size">
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
|
||||
class="block-size">
|
||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
|
||||
class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
|
||||
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }}
|
||||
transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }}
|
||||
transactions</ng-template>
|
||||
</div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||
</div>
|
||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #placeholderBlock>
|
||||
<ng-container *ngIf="block && block.placeholder; else loadingBlock">
|
||||
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block placeholder-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]">
|
||||
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i"
|
||||
class="text-center bitcoin-block mined-block placeholder-block blockchain-blocks-{{ i }}"
|
||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]">
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #loadingBlock>
|
||||
<ng-container *ngIf="block && block.loading">
|
||||
<div class="flashing">
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"></div>
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
|
||||
[ngStyle]="blockStyles[i]"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition"
|
||||
[ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingBlocksTemplate>
|
||||
<div class="blocks-container" [class.time-ltr]="timeLtr">
|
||||
<div class="flashing">
|
||||
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn" >
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="emptyBlockStyles[i]"></div>
|
||||
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
|
||||
[ngStyle]="emptyBlockStyles[i]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
|
||||
<th *ngIf="indexingAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||
@@ -45,7 +45,7 @@
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<a
|
||||
class="health-badge badge"
|
||||
[class.badge-success]="auditScores[block.id] >= 99"
|
||||
@@ -98,7 +98,7 @@
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<td *ngIf="auditAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
@@ -123,8 +123,10 @@
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
<ng-template [ngIf]="!widget">
|
||||
<div class="clearfix"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
latestScoreSubscription: Subscription;
|
||||
|
||||
indexingAvailable = false;
|
||||
auditAvailable = false;
|
||||
isLoading = true;
|
||||
loadingScores = true;
|
||||
fromBlockHeight = undefined;
|
||||
@@ -44,6 +45,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
|
||||
this.stateService.env.MINING_DASHBOARD === true);
|
||||
this.auditAvailable = this.indexingAvailable && this.stateService.env.AUDIT;
|
||||
|
||||
if (!this.widget) {
|
||||
this.websocketService.want(['blocks']);
|
||||
@@ -111,7 +113,7 @@ export class BlocksList implements OnInit, OnDestroy {
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
if (this.indexingAvailable && this.auditAvailable) {
|
||||
this.auditScoreSubscription = this.fromHeightSubject.pipe(
|
||||
switchMap((fromBlockHeight) => {
|
||||
this.loadingScores = true;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div [formGroup]="fiatForm" class="text-small text-center">
|
||||
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeFiat()">
|
||||
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { fiatCurrencies } from '../../app.constants';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fiat-selector',
|
||||
templateUrl: './fiat-selector.component.html',
|
||||
styleUrls: ['./fiat-selector.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FiatSelectorComponent implements OnInit {
|
||||
fiatForm: UntypedFormGroup;
|
||||
currencies = Object.entries(fiatCurrencies).sort((a: any, b: any) => {
|
||||
if (a[1].name < b[1].name) {
|
||||
return -1;
|
||||
}
|
||||
if (a[1].name > b[1].name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.fiatForm = this.formBuilder.group({
|
||||
fiat: ['USD']
|
||||
});
|
||||
this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.fiatForm.get('fiat')?.setValue(fiat);
|
||||
});
|
||||
}
|
||||
|
||||
changeFiat() {
|
||||
const newFiat = this.fiatForm.get('fiat')?.value;
|
||||
this.storageService.setValue('fiat-preference', newFiat);
|
||||
this.stateService.fiatCurrency$.next(newFiat);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
i18n="mining.block-rewards">Block Rewards</a>
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
|
||||
<a class="dropdown-item" routerLinkActive="active"
|
||||
<a *ngIf="stateService.env.AUDIT" class="dropdown-item" routerLinkActive="active"
|
||||
[routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" i18n="mining.block-prediction-accuracy">Block Prediction Accuracy</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div [formGroup]="languageForm" class="text-small text-center">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 130px;" (change)="changeLanguage()">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeLanguage()">
|
||||
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -74,4 +74,24 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pref-selectors">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-of-service">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
|
|
||||
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
|
||||
|
|
||||
<a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -103,4 +103,23 @@
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pref-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.selector {
|
||||
margin-left: .5em;
|
||||
margin-bottom: .5em;
|
||||
&:first {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
// 'Other'
|
||||
data.push({
|
||||
itemStyle: {
|
||||
color: 'grey',
|
||||
color: '#6b6b6b',
|
||||
},
|
||||
value: totalShareOther,
|
||||
name: 'Other' + (isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
|
||||
|
||||
@@ -235,10 +235,10 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="reward text-right">
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
|
||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td class="fees text-right">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td class="txs text-right">
|
||||
{{ block.tx_count | number }}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain>
|
||||
</div>
|
||||
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
|
||||
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true" i18n-title="blocks.return-to-tip" title="Return to tip"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -300,6 +300,10 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.blockchainContainer?.nativeElement) {
|
||||
// clean up scroll position to prevent caching wrong scroll in Firefox
|
||||
this.blockchainContainer.nativeElement.scrollLeft = 0;
|
||||
}
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
|
||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
|
||||
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -144,7 +144,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-language-selector></app-language-selector>
|
||||
<div class="pref-selectors">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-of-service">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
|
||||
@@ -323,4 +323,19 @@
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.pref-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.selector {
|
||||
margin-left: .5em;
|
||||
margin-bottom: .5em;
|
||||
&:first {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
@@ -7,7 +7,6 @@ import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { WebsocketService } from '../services/websocket.service';
|
||||
import { SeoService } from '../services/seo.service';
|
||||
import { StorageService } from '../services/storage.service';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
@@ -32,7 +31,7 @@ interface MempoolStatsData {
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
featuredAssets$: Observable<any>;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
@@ -47,16 +46,20 @@ export class DashboardComponent implements OnInit {
|
||||
transactionsWeightPerSecondOptions: any;
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
liquidPegsMonth$: Observable<any>;
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
private storageService: StorageService,
|
||||
private seoService: SeoService
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.seoService.resetTitle();
|
||||
@@ -213,6 +216,10 @@ export class DashboardComponent implements OnInit {
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
||||
|
||||
@@ -8667,6 +8667,22 @@ export const faqData = [
|
||||
fragment: "why-dont-fee-ranges-match",
|
||||
title: "Why doesn't the fee range shown for a block match the feerates of transactions within the block?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "advanced",
|
||||
showConditions: bitcoinNetworks,
|
||||
options: { auditOnly: true },
|
||||
fragment: "how-do-block-audits-work",
|
||||
title: "How do block audits work?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "advanced",
|
||||
showConditions: bitcoinNetworks,
|
||||
options: { auditOnly: true },
|
||||
fragment: "what-is-block-health",
|
||||
title: "What is block health?",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
category: "self-hosting",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div *ngFor="let item of tabData">
|
||||
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</p>
|
||||
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
|
||||
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { restApiDocsData } from './api-docs-data';
|
||||
import { faqData } from './api-docs-data';
|
||||
|
||||
@@ -12,11 +13,17 @@ export class ApiDocsNavComponent implements OnInit {
|
||||
@Input() network: any;
|
||||
@Input() whichTab: string;
|
||||
@Output() navLinkClickEvent: EventEmitter<any> = new EventEmitter();
|
||||
env: Env;
|
||||
tabData: any[];
|
||||
auditEnabled: boolean;
|
||||
|
||||
constructor() { }
|
||||
constructor(
|
||||
private stateService: StateService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.auditEnabled = this.env.AUDIT;
|
||||
if (this.whichTab === 'rest') {
|
||||
this.tabData = restApiDocsData;
|
||||
} else if (this.whichTab === 'faq') {
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
</div>
|
||||
|
||||
<div class="doc-item-container" *ngFor="let item of faq">
|
||||
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
|
||||
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
|
||||
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a>
|
||||
<div class="endpoint-content">
|
||||
<ng-container *ngTemplateOutlet="dict[item.fragment]" class="endpoint"></ng-container>
|
||||
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled )">
|
||||
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
|
||||
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
|
||||
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a>
|
||||
<div class="endpoint-content">
|
||||
<ng-container *ngTemplateOutlet="dict[item.fragment]" class="endpoint"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,8 +216,46 @@
|
||||
|
||||
<ng-template type="why-dont-fee-ranges-match">
|
||||
<p>Mempool aims to show you the <i>effective feerate</i> range for blocks—how much would you actually need to pay to get a transaction included in a block.</p>
|
||||
<p>A transaction's effective feerate is not always the same as the feerate explicitly set for it. For example, if you see a 1 s/vb transaction in a block with a displayed feerate range of 5 s/vb to 72 s/vb, chances are that 1 s/vb transaction had a high-feerate child transaction that boosted its effective feerate to 5 s/vb or higher (this is how CPFP fee-bumping works). In such a case, it would be misleading to use 1 s/vb as the lower bound of the block's feerate range because it actually required more than 1 s/vb to confirm that transaction in that block.</p>
|
||||
<p>For unconfirmed CPFP transactions, Mempool will show the effective feerate (along with descendent & ancestor transaction information) on the transaction page. For confirmed transactions, CPFP relationships are not stored, so this additional information is not shown.</p>
|
||||
<p>A transaction's effective feerate is not always the same as the feerate explicitly set for it. For example, if you see a 1 s/vb transaction in a block with a displayed feerate range of 5 s/vb to 72 s/vb, chances are that 1 s/vb transaction had a high-feerate child transaction that boosted its effective feerate to 5 s/vb or higher (this is how CPFP fee-bumping works). In such a case, it would be misleading to use 1 s/vb as the lower bound of the block's feerate range since it actually required more than 1 s/vb to confirm that transaction in that block.</p>
|
||||
<p>You can find a transaction's feerate on its transaction details page. If the transaction has any CPFP relationships, the page will also show the transaction's effective feerate along with links to descendent and/or ancestor transactions.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-do-block-audits-work">
|
||||
<p>A block audit visually compares Mempool's expected block to the actual block for a particular block height.</p>
|
||||
<p>How is the expected block determined? Mempool monitors its view of the mempool and runs a re-implementation of Bitcoin Core's transaction selection algorithm to determine the transactions it expects to see in upcoming blocks (<a href="https://github.com/mempool/mempool/blob/master/backend/src/api/mempool-blocks.ts" target="_blank">source code here</a>). Since there is a continual flow of new transactions, this algorithm runs every 2 seconds, and as a result, you will see the transactions <a href="/mempool-block/0">projected to be in upcoming blocks</a> change in near real-time.</p>
|
||||
<p>At the moment a new block is mined, Mempool saves a snapshot of its projected block template for the next block. We call this snapshot the <b>expected block</b> for the block height in question, and it serves as the basis for the block audit.</p>
|
||||
<p>When details for an expected block and actual block are available, we can compare them. <b>The purpose of block audits is to deduce when miners intentionally include or exclude transactions from blocks they mine.</b> Since this information cannot be precisely known, Mempool uses a handful of heuristics to accomplish this.</p>
|
||||
<p>Block audits highlight transactions in different colors to convey these heuristics:</p>
|
||||
<ul class="no-bull block-audit">
|
||||
<li><span class="block-audit-highlight-color added">■</span><code>Added</code><p>A transaction is highlighted blue if it is not present in the expected block, present in the actual block, and also either:</p>
|
||||
<ul>
|
||||
<li>far out of the expected feerate range, meaning the miner may have intentionally prioritized the transaction</li>
|
||||
<li>not in the mempool at all, meaning the miner may have accepted the transaction out-of-band</li>
|
||||
</ul>
|
||||
<p>Added transactions do not negatively affect <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-block-health">block health</a>.</p></li>
|
||||
<li><span class="block-audit-highlight-color recent">■</span><code>Recently broadcasted</code><p>A transaction is highlighted dark pink if it is present in the expected block, not present in the actual block, and was first seen by Mempool's Bitcoin node within 3 minutes of the block being mined.</p><p>Due to network latency and other factors, it can take time for a miner's Bitcoin nodes to receive a transaction, so we do not assume a miner has intentionally excluded such a transaction from a block.</p><p>Recently-broadcasted transactions do not negatively affect <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-block-health">block health</a>.</p></li>
|
||||
<li><span class="block-audit-highlight-color marginal">■</span><code>Marginal fee</code>
|
||||
<p>A transaction is darkened if it is in the low end of the expected feerate range and missing in either the expected block or the actual block.</p><p>Such a transaction may have been displaced by an added transaction, or it may have been displaced by another transaction from the mempool that was also at the low end of the expected feerate range for the block. In either case, the deviation is not considered notable.</p>
|
||||
<p>Marginal fee transactions do not negatively affect <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-block-health">block health</a>.</p></li>
|
||||
<li><span class="block-audit-highlight-color removed">■</span><code>Removed</code><p>A transaction is highlighted bright pink if it is present in the expected block, not present in the actual block, and qualifies as neither recently-broadcasted nor marginal-fee. In other words, it has been in the mempool long enough to be widely propagated and has a feerate that is well within the range expected for the block. There is a chance such a transaction may have been intentionally excluded from the block.<p>Removed transactions do negatively affect <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-block-health">block health</a>.</p></li>
|
||||
</ul>
|
||||
<p>See how results of the block audit are used to devise the block health score <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="what-is-block-health">below</a>.</p>
|
||||
<p class='note'>Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="what-is-block-health">
|
||||
<p>Block health is a measure of how many transactions appear to be intentionally excluded from a block—a block without any transactions that appear intentionally excluded will have 100% health, while a block with 1 or more transactions that appear intentionally excluded will have sub-100% health.</p>
|
||||
<p>How is it calculated? Let <span class='math'>s<sub>expected</sub></span> be the set of all transactions in Mempool's expected block and let <span class='math'>s<sub>actual</sub></span> be the set of all transactions in the actual block. Then let <span class='math'>n</span> be the number of all transactions in both <span class='math'>s<sub>expected</sub></span> and <span class='math'>s<sub>actual</sub></span>.</p>
|
||||
<p>Furthermore, let <span class='math'>r</span> be the number of transactions Mempool deduces were <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-block-audits-work">intentionally excluded</a> from <span class='math'>s<sub>actual</sub></span>.</p>
|
||||
<p>Block health is calculated as <span class='math'>n / ( n + r</span> ).</p>
|
||||
<p>The number of transactions appearing in both <span class='math'>s<sub>expected</sub></span> and <span class='math'>s<sub>actual</sub></span> is used (instead of a block's full transaction count) in order to minimize chances that block health is inadvertently impacted by transactions that were most likely not intentionally excluded:</p>
|
||||
<ul>
|
||||
<li>recently-broadcast transactions, since the miner may simply not have received them</li>
|
||||
<li>certain low-feerate transactions, since the miner may have opted to replace them with more profitable out-of-band transactions</li>
|
||||
</ul>
|
||||
<p>As a result, block health is <i>not</i> intended to be a measure of how closely an expected block resembles an actual block. The actual block can be vastly different from the expected block, but if no transactions appear to be intentionally excluded, it will have a high health rating (<a [routerLink]="['/block/0000000000000000000515e202c8ae73c8155fc472422d7593af87aa74f2cf3d']">extreme example</a>).</p>
|
||||
<p>See more context in our FAQ on <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-block-audits-work">block audits</a>.</p>
|
||||
<p class='note'>Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="who-runs-this-website">
|
||||
|
||||
@@ -21,6 +21,12 @@ code {
|
||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
|
||||
}
|
||||
|
||||
.math {
|
||||
font-family: monospace;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
tr {
|
||||
white-space: inherit;
|
||||
}
|
||||
@@ -36,6 +42,48 @@ li.nav-item {
|
||||
}
|
||||
}
|
||||
|
||||
ul.no-bull {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li code {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li span.block-audit-highlight-color.added {
|
||||
color: #0099ff;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li span.block-audit-highlight-color.removed {
|
||||
color: #f344df;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li span.block-audit-highlight-color.recent {
|
||||
color: #8a3480;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li span.block-audit-highlight-color.marginal {
|
||||
color: #414127;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li p {
|
||||
margin-left: 25px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit li ul {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
ul.no-bull.block-audit code{
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.doc-welcome-note {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, OnInit, Input, QueryList, AfterViewInit, ViewChildren } from '@angular/core';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable, merge, of } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Observable, merge, of, Subject } from 'rxjs';
|
||||
import { tap, takeUntil } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { faqData, restApiDocsData, wsApiDocsData } from './api-docs-data';
|
||||
import { FaqTemplateDirective } from '../faq-template/faq-template.component';
|
||||
@@ -12,6 +12,7 @@ import { FaqTemplateDirective } from '../faq-template/faq-template.component';
|
||||
styleUrls: ['./api-docs.component.scss']
|
||||
})
|
||||
export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
private destroy$: Subject<any> = new Subject<any>();
|
||||
plainHostname = document.location.hostname;
|
||||
electrsPort = 0;
|
||||
hostname = document.location.hostname;
|
||||
@@ -27,6 +28,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
wsDocs: any;
|
||||
screenWidth: number;
|
||||
officialMempoolInstance: boolean;
|
||||
auditEnabled: boolean;
|
||||
|
||||
@ViewChildren(FaqTemplateDirective) faqTemplates: QueryList<FaqTemplateDirective>;
|
||||
dict = {};
|
||||
@@ -59,6 +61,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
ngOnInit(): void {
|
||||
this.env = this.stateService.env;
|
||||
this.officialMempoolInstance = this.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
this.auditEnabled = this.env.AUDIT;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$).pipe(
|
||||
tap((network: string) => {
|
||||
if (this.env.BASE_MODULE === 'mempool' && network !== '') {
|
||||
@@ -82,7 +85,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
this.restDocs = restApiDocsData;
|
||||
this.wsDocs = wsApiDocsData;
|
||||
|
||||
this.network$.subscribe((network) => {
|
||||
this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => {
|
||||
this.active = (network === 'liquid' || network === 'liquidtestnet') ? 2 : 0;
|
||||
switch( network ) {
|
||||
case "":
|
||||
@@ -102,6 +105,8 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next(true);
|
||||
this.destroy$.complete();
|
||||
window.removeEventListener('scroll', this.onDocScroll);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<span class="green-color">{{ (conversions$ | async)?.USD * value / 100000000 | currency:'USD':'symbol':digitsInfo }}</span>
|
||||
<span class="green-color" *ngIf="(conversions$ | async) as conversions">{{ (conversions ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { StateService } from '../services/state.service';
|
||||
|
||||
@Component({
|
||||
@@ -8,18 +8,30 @@ import { StateService } from '../services/state.service';
|
||||
styleUrls: ['./fiat.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FiatComponent implements OnInit {
|
||||
export class FiatComponent implements OnInit, OnDestroy {
|
||||
conversions$: Observable<any>;
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
@Input() value: number;
|
||||
@Input() digitsInfo = '1.2-2';
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -151,6 +151,19 @@ export interface RewardStats {
|
||||
totalTx: number;
|
||||
}
|
||||
|
||||
export interface BlockSizesAndWeights {
|
||||
sizes: {
|
||||
timestamp: number;
|
||||
avgHeight: number;
|
||||
avgSize: number;
|
||||
}[];
|
||||
weights: {
|
||||
timestamp: number;
|
||||
avgHeight: number;
|
||||
avgWeight: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string;
|
||||
matchRate?: number;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="box-right">
|
||||
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>
|
||||
<div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount></div>
|
||||
<div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2" [noFiat]="true"></app-amount></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<span i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
|
||||
<app-fiat [value]="statistics.latest?.avg_capacity" digitsInfo="1.0-0" ></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<span i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
|
||||
<app-fiat [value]="statistics.latest?.med_capacity" digitsInfo="1.0-0" ></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,11 +84,24 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-small text-center mt-1" *ngIf="officialMempoolSpace">
|
||||
<a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
|
||||
<div class="pref-selectors">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
<div class="selector">
|
||||
<app-fiat-selector></app-fiat-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-of-service">
|
||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||
|
|
||||
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
|
||||
|
|
||||
<a *ngIf="officialMempoolSpace" [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
|
||||
<a *ngIf="!officialMempoolSpace" [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
@@ -103,4 +103,23 @@
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pref-selectors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
.selector {
|
||||
margin-left: .5em;
|
||||
margin-bottom: .5em;
|
||||
&:first {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,10 @@
|
||||
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||
[disableTooltip]="!statistics.previous" placement="bottom">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
|
||||
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.total_capacity" [previous]="statistics.previous?.total_capacity">
|
||||
</app-change>
|
||||
<app-fiat [value]="statistics.latest?.total_capacity" digitsInfo="1.0-0" ></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
@@ -222,8 +222,8 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalBlockSizesAndWeights$(interval: string | undefined) : Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
getHistoricalBlockSizesAndWeights$(interval: string | undefined) : Observable<HttpResponse<BlockSizesAndWeights>> {
|
||||
return this.httpClient.get<BlockSizesAndWeights>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/sizes-weights` +
|
||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface Env {
|
||||
BISQ_WEBSITE_URL: string;
|
||||
MINING_DASHBOARD: boolean;
|
||||
LIGHTNING: boolean;
|
||||
AUDIT: boolean;
|
||||
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
@@ -67,6 +68,7 @@ const defaultEnv: Env = {
|
||||
'BISQ_WEBSITE_URL': 'https://bisq.markets',
|
||||
'MINING_DASHBOARD': true,
|
||||
'LIGHTNING': false,
|
||||
'AUDIT': false,
|
||||
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||
@@ -119,6 +121,7 @@ export class StateService {
|
||||
timeLtr: BehaviorSubject<boolean>;
|
||||
hideFlow: BehaviorSubject<boolean>;
|
||||
hideAudit: BehaviorSubject<boolean>;
|
||||
fiatCurrency$: BehaviorSubject<string>;
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@@ -184,6 +187,9 @@ export class StateService {
|
||||
this.hideAudit.subscribe((hide) => {
|
||||
this.storageService.setValue('audit-preference', hide ? 'hide' : 'show');
|
||||
});
|
||||
|
||||
const fiatPreference = this.storageService.getValue('fiat-preference');
|
||||
this.fiatCurrency$ = new BehaviorSubject<string>(fiatPreference || 'USD');
|
||||
}
|
||||
|
||||
setNetworkBasedonUrl(url: string) {
|
||||
|
||||
28
frontend/src/app/shared/pipes/fiat-currency.pipe.ts
Normal file
28
frontend/src/app/shared/pipes/fiat-currency.pipe.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { formatCurrency, getCurrencySymbol } from '@angular/common';
|
||||
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'fiatCurrency'
|
||||
})
|
||||
export class FiatCurrencyPipe implements PipeTransform {
|
||||
fiatSubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.fiatSubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
}
|
||||
|
||||
transform(num: number, ...args: any[]): unknown {
|
||||
const digits = args[0] || 1;
|
||||
const currency = args[1] || this.currency || 'USD';
|
||||
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
import { formatCurrency, getCurrencySymbol } from '@angular/common';
|
||||
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'fiatShortener'
|
||||
})
|
||||
export class FiatShortenerPipe implements PipeTransform {
|
||||
fiatSubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string
|
||||
) {}
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.fiatSubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
}
|
||||
|
||||
transform(num: number, ...args: any[]): unknown {
|
||||
const digits = args[0] || 1;
|
||||
const unit = args[1] || undefined;
|
||||
const currency = args[1] || this.currency || 'USD';
|
||||
|
||||
if (num < 1000) {
|
||||
return num.toFixed(digits);
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 1 }).format(num);
|
||||
}
|
||||
|
||||
const lookup = [
|
||||
@@ -30,8 +40,8 @@ export class FiatShortenerPipe implements PipeTransform {
|
||||
const item = lookup.slice().reverse().find((item) => num >= item.value);
|
||||
|
||||
let result = item ? (num / item.value).toFixed(digits).replace(rx, '$1') : '0';
|
||||
result = formatCurrency(parseInt(result, 10), this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0');
|
||||
|
||||
result = new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(item ? num / item.value : 0);
|
||||
|
||||
return result + item.symbol;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
|
||||
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
|
||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
|
||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
|
||||
import { FiatCurrencyPipe } from './pipes/fiat-currency.pipe';
|
||||
import { BlockchainComponent } from '../components/blockchain/blockchain.component';
|
||||
import { TimeSinceComponent } from '../components/time-since/time-since.component';
|
||||
import { TimeUntilComponent } from '../components/time-until/time-until.component';
|
||||
@@ -34,6 +35,7 @@ import { TxFeaturesComponent } from '../components/tx-features/tx-features.compo
|
||||
import { TxFeeRatingComponent } from '../components/tx-fee-rating/tx-fee-rating.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { LanguageSelectorComponent } from '../components/language-selector/language-selector.component';
|
||||
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
|
||||
import { ColoredPriceDirective } from './directives/colored-price.directive';
|
||||
import { NoSanitizePipe } from './pipes/no-sanitize.pipe';
|
||||
import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blocks.component';
|
||||
@@ -93,6 +95,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
TxFeaturesComponent,
|
||||
TxFeeRatingComponent,
|
||||
LanguageSelectorComponent,
|
||||
FiatSelectorComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
NoSanitizePipe,
|
||||
@@ -107,6 +110,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
CapAddressPipe,
|
||||
Decimal2HexPipe,
|
||||
FeeRoundingPipe,
|
||||
FiatCurrencyPipe,
|
||||
ColoredPriceDirective,
|
||||
BlockchainComponent,
|
||||
MempoolBlocksComponent,
|
||||
@@ -199,6 +203,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
TxFeaturesComponent,
|
||||
TxFeeRatingComponent,
|
||||
LanguageSelectorComponent,
|
||||
FiatSelectorComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
RelativeUrlPipe,
|
||||
Hex2asciiPipe,
|
||||
@@ -207,6 +212,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
FiatCurrencyPipe,
|
||||
CeilPipe,
|
||||
ShortenStringPipe,
|
||||
CapAddressPipe,
|
||||
|
||||
Reference in New Issue
Block a user