Merge branch 'master' into qrcodes

This commit is contained in:
wiz
2022-07-06 23:31:01 +02:00
committed by GitHub
108 changed files with 7508 additions and 3026 deletions

3
frontend/.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
frontend

34
frontend/.eslintrc Normal file
View File

@@ -0,0 +1,34 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/ban-ts-comment": 1,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 1,
"@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"no-case-declarations": 1,
"no-console": 1,
"no-constant-condition": 1,
"no-dupe-else-if": 1,
"no-empty": 1,
"no-extra-boolean-cast": 1,
"no-prototype-builtins": 1,
"no-self-assign": 1,
"no-useless-catch": 1,
"no-var": 1,
"prefer-const": 1,
"prefer-rest-params": 1
}
}

View File

@@ -248,23 +248,6 @@
"browserTarget": "mempool:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/resources"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"e2e": {
"builder": "@cypress/schematic:cypress",
"options": {

View File

@@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/mempool'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,8 @@
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
"test": "npm run ng -- test",
"lint": "npm run ng -- lint",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"e2e": "npm run generate-config && npm run ng -- e2e",
"e2e:ci": "npm run cypress:run:ci",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
@@ -77,7 +78,6 @@
"@fortawesome/fontawesome-common-types": "~6.1.1",
"@fortawesome/fontawesome-svg-core": "~6.1.1",
"@fortawesome/free-solid-svg-icons": "~6.1.1",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@nguniversal/express-engine": "~13.1.1",
@@ -88,7 +88,7 @@
"domino": "^2.1.6",
"echarts": "~5.3.2",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"lightweight-charts": "~3.8.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1",
@@ -104,28 +104,23 @@
"@angular/language-service": "~13.3.10",
"@nguniversal/builders": "~13.1.1",
"@types/express": "^4.17.0",
"@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^12.11.1",
"codelyzer": "~6.0.2",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~4.1.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.3.19",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~5.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"eslint": "^8.19.0",
"http-proxy-middleware": "~2.0.6",
"ts-node": "~10.8.1",
"typescript": "~4.6.4"
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"@cypress/schematic": "~2.0.0",
"cypress": "^10.0.2",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-fail-on-console-error": "~2.1.4",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",
"start-server-and-test": "^1.12.6"
"mock-socket": "~9.1.4",
"start-server-and-test": "~1.14.0"
},
"scarfSettings": {
"enabled": false
}
}

View File

@@ -171,7 +171,7 @@ let routes: Routes = [
{
path: 'block',
component: StartComponent,
children: [
children: [
{
path: ':id',
component: BlockComponent
@@ -258,7 +258,7 @@ let routes: Routes = [
{
path: 'block',
component: StartComponent,
children: [
children: [
{
path: ':id',
component: BlockComponent
@@ -361,7 +361,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'block',
component: StartComponent,
children: [
children: [
{
path: ':id',
component: BlockComponent
@@ -465,7 +465,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'block',
component: StartComponent,
children: [
children: [
{
path: ':id',
component: BlockComponent

View File

@@ -31,7 +31,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
}
ngOnChanges() {
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);;
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
}
switchCurrency() {

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddressLabelsComponent } from './address-labels.component';
describe('AddressLabelsComponent', () => {
let component: AddressLabelsComponent;
let fixture: ComponentFixture<AddressLabelsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddressLabelsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddressLabelsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -130,7 +130,7 @@
<span i18n="address.error.loading-address-data">Error loading address data.</span>
<br>
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError">
<ng-template [ngIf]="error.status === 413 || error.status === 405 || error.status === 504" [ngIfElse]="displayServerError">
<ng-container i18n="Electrum server limit exceeded error">
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
<br><br>

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AmountComponent } from './amount.component';
describe('AmountComponent', () => {
let component: AmountComponent;
let fixture: ComponentFixture<AmountComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AmountComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AmountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,35 +0,0 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'mempool'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('mempool');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to mempool!');
});
});

View File

@@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -79,57 +79,3 @@
}
}
}
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@@ -174,7 +174,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: function (data) {
formatter: function(data) {
if (data.length <= 0) {
return '';
}

View File

@@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -79,57 +79,3 @@
}
}
}
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@@ -0,0 +1,52 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-prediction-accuracy">Block Predictions Accuracy</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 24h
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3D
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1W
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> ALL
</label>
</div>
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -0,0 +1,81 @@
.card-header {
border-bottom: 0;
font-size: 18px;
@media (min-width: 465px) {
font-size: 20px;
}
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
padding: 0px 15px;
width: 100%;
min-height: 500px;
height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
};
}
.chart {
width: 100%;
height: 100%;
padding-bottom: 20px;
padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}

View File

@@ -0,0 +1,289 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { StorageService } from 'src/app/services/storage.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-block-predictions-graph',
templateUrl: './block-predictions-graph.component.html',
styleUrls: ['./block-predictions-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockPredictionsGraphComponent implements OnInit {
@Input() right: number | string = 45;
@Input() left: number | string = 75;
miningWindowPreference: string;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
statsObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
private storageService: StorageService,
private zone: NgZone,
private route: ActivatedRoute,
private stateService: StateService,
private router: Router,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
this.seoService.setTitle($localize`Block predictions accuracy`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route
.fragment
.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
this.isLoading = true;
return this.apiService.getHistoricalBlockPrediction$(timespan)
.pipe(
tap((response) => {
this.prepareChartOptions(response.body);
this.isLoading = false;
}),
map((response) => {
return {
blockCount: parseInt(response.headers.get('x-total-count'), 10),
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
animation: false,
grid: {
top: 30,
bottom: 80,
right: this.right,
left: this.left,
},
tooltip: {
show: !this.isMobile(),
trigger: 'axis',
axisPointer: {
type: 'line'
},
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10) * 1000)}</b><br>`;
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data.value, this.locale, '1.2-2')}%<br>`;
if (['24h', '3d'].includes(this.timespan)) {
tooltip += `<small>` + $localize`At block: ${ticks[0].data.block}` + `</small>`;
} else {
tooltip += `<small>` + $localize`Around block: ${ticks[0].data.block}` + `</small>`;
}
return tooltip;
}
},
xAxis: {
name: formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
align: 'center',
fontSize: 11,
lineHeight: 12,
hideOverlap: true,
padding: [0, 5],
},
data: data.map(prediction => prediction[0])
},
yAxis: [
{
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
return `${val}%`;
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
},
],
series: [
{
zlevel: 0,
name: $localize`Match rate`,
data: data.map(prediction => ({
value: prediction[2],
block: prediction[1],
itemStyle: {
color: this.getPredictionColor(prediction[2])
}
})),
type: 'bar',
barWidth: '90%',
},
],
dataZoom: [{
type: 'inside',
realtime: true,
zoomLock: true,
maxSpan: 100,
minSpan: 5,
moveOnMouseMove: false,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
left: 20,
right: 15,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
},
}],
};
}
colorGradient(fadeFraction, rgbColor1, rgbColor2, rgbColor3) {
let color1 = rgbColor1;
let color2 = rgbColor2;
let fade = fadeFraction;
// Do we have 3 colors for the gradient? Need to adjust the params.
if (rgbColor3) {
fade = fade * 2;
// Find which interval to use and adjust the fade percentage
if (fade >= 1) {
fade -= 1;
color1 = rgbColor2;
color2 = rgbColor3;
}
}
const diffRed = color2.red - color1.red;
const diffGreen = color2.green - color1.green;
const diffBlue = color2.blue - color1.blue;
const gradient = {
red: Math.floor(color1.red + (diffRed * fade)),
green: Math.floor(color1.green + (diffGreen * fade)),
blue: Math.floor(color1.blue + (diffBlue * fade)),
};
return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
}
getPredictionColor(matchRate) {
return this.colorGradient(
Math.pow((100 - matchRate) / 100, 0.5),
{red: 67, green: 171, blue: 71},
{red: 253, green: 216, blue: 53},
{red: 244, green: 0, blue: 0},
);
}
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
this.zone.run(() => {
if (['24h', '3d'].includes(this.timespan)) {
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data.block}`);
this.router.navigate([url]);
}
});
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@@ -36,7 +36,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -79,57 +79,3 @@
}
}
}
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -79,57 +79,3 @@
}
}
}
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@@ -50,7 +50,7 @@
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
@@ -358,7 +358,7 @@
<div class="text-center">
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error.code }}: {{ error.error }}</i>
<i>{{ error.status }}: {{ error.error }}</i>
</div>
</ng-template>

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClipboardComponent } from './clipboard.component';
describe('ClipboardComponent', () => {
let component: ClipboardComponent;
let fixture: ComponentFixture<ClipboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ClipboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClipboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -9,7 +9,7 @@
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data.difficulty">
<tr *ngFor="let diffChange of data">
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
}}</a></td>
<td class="text-left">
@@ -17,7 +17,7 @@
</td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
{{ diffChange.change >= 0 ? '+' : '' }}{{ diffChange.change | amountShortener }}%
{{ diffChange.change >= 0 ? '+' : '' }}{{ diffChange.change | amountShortener: 2 }}%
</td>
</tr>
</tbody>

View File

@@ -30,27 +30,24 @@ export class DifficultyAdjustmentsTable implements OnInit {
}
ngOnInit(): void {
this.hashrateObservable$ = this.apiService.getHistoricalHashrate$('1y')
this.hashrateObservable$ = this.apiService.getDifficultyAdjustments$('3m')
.pipe(
map((response) => {
const data = response.body;
const tableData = [];
for (let i = data.difficulty.length - 1; i > 0; --i) {
const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty);
const change = (data.difficulty[i].difficulty / data.difficulty[i - 1].difficulty - 1) * 100;
tableData.push(Object.assign(data.difficulty[i], {
change: Math.round(change * 100) / 100,
for (const adjustment of data) {
const selectedPowerOfTen: any = selectPowerOfTen(adjustment[2]);
tableData.push({
height: adjustment[1],
timestamp: adjustment[0],
change: (adjustment[3] - 1) * 100,
difficultyShorten: formatNumber(
data.difficulty[i].difficulty / selectedPowerOfTen.divider,
adjustment[2] / selectedPowerOfTen.divider,
this.locale, '1.2-2') + selectedPowerOfTen.unit
}));
});
}
this.isLoading = false;
return {
difficulty: tableData.slice(0, 6),
};
return tableData.slice(0, 6);
}),
);
}

View File

@@ -1,4 +1,4 @@
<div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-flex menu" style="padding: 0px 35px;">
<div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-inline-flex menu" style="padding: 0px 35px;">
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1"
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
<div ngbDropdown class="w-50">
@@ -18,6 +18,8 @@
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" 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"
[routerLink]="['/graphs/mining/block-predictions' | relativeUrl]" i18n="mining.block-prediction-accuracy">Blocks Predictions Accuracy</a>
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -11,6 +11,7 @@ import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils';
import { ActivatedRoute } from '@angular/router';
import { StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-hashrate-chart',
@@ -47,7 +48,7 @@ export class HashrateChartComponent implements OnInit {
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
maResolution: number = 30;
network = '';
constructor(
@Inject(LOCALE_ID) public locale: string,
@@ -57,17 +58,20 @@ export class HashrateChartComponent implements OnInit {
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
private stateService: StateService
) {
}
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
let firstRun = true;
if (this.widget) {
this.miningWindowPreference = '1y';
} else {
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -95,6 +99,7 @@ export class HashrateChartComponent implements OnInit {
.pipe(
tap((response) => {
const data = response.body;
// We generate duplicated data point so the tooltip works nicely
const diffFixed = [];
let diffIndex = 1;
@@ -112,7 +117,7 @@ export class HashrateChartComponent implements OnInit {
}
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].timestamp
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
@@ -123,17 +128,14 @@ export class HashrateChartComponent implements OnInit {
++diffIndex;
}
this.maResolution = 30;
if (["3m", "6m"].includes(this.timespan)) {
this.maResolution = 7;
}
let maResolution = 15;
const hashrateMa = [];
for (let i = this.maResolution - 1; i < data.hashrates.length; ++i) {
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
let avg = 0;
for (let y = this.maResolution - 1; y >= 0; --y) {
for (let y = maResolution - 1; y >= 0; --y) {
avg += data.hashrates[i - y].avgHashrate;
}
avg /= this.maResolution;
avg /= maResolution;
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
}
@@ -275,17 +277,17 @@ export class HashrateChartComponent implements OnInit {
},
},
{
name: $localize`::Difficulty`,
name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
},
{
name: $localize`Hashrate` + ` (MA${this.maResolution})`,
name: $localize`Hashrate (MA)`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
@@ -294,11 +296,18 @@ export class HashrateChartComponent implements OnInit {
},
},
],
selected: JSON.parse(this.storageService.getValue('hashrate_difficulty_legend')) ?? {
'$localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`': true,
'$localize`::Difficulty`': this.network === '',
'$localize`Hashrate (MA)`': true,
},
},
yAxis: data.hashrates.length === 0 ? undefined : [
{
min: (value) => {
return value.min * 0.9;
const selectedPowerOfTen: any = selectPowerOfTen(value.min);
const newMin = Math.floor(value.min / selectedPowerOfTen.divider / 10);
return newMin * selectedPowerOfTen.divider * 10;
},
type: 'value',
axisLabel: {
@@ -362,7 +371,7 @@ export class HashrateChartComponent implements OnInit {
},
{
zlevel: 2,
name: $localize`Hashrate` + ` (MA${this.maResolution})`,
name: $localize`Hashrate (MA)`,
showSymbol: false,
symbol: 'none',
data: data.hashrateMa,
@@ -403,6 +412,10 @@ export class HashrateChartComponent implements OnInit {
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('legendselectchanged', (e) => {
this.storageService.setValue('hashrate_difficulty_legend', JSON.stringify(e.selected));
});
}
isMobile() {

View File

@@ -21,7 +21,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> ALL
</label>
</div>

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MempoolBlockComponent } from './mempool-block.component';
describe('MempoolBlockComponent', () => {
let component: MempoolBlockComponent;
let fixture: ComponentFixture<MempoolBlockComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MempoolBlockComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MempoolBlockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -40,7 +40,7 @@
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [widget]="true"></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" fragment="1y" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View File

@@ -66,7 +66,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount > 157680">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]"><span i18n>All</span>
</label>
</div>

View File

@@ -1,5 +1,5 @@
<div class="container-xl">
<h1 i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</h1>
<h1 class="text-left" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</h1>
<form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate>
<div class="mb-3">

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { QrcodeComponent } from './qrcode.component';
describe('QrcodeComponent', () => {
let component: QrcodeComponent;
let fixture: ComponentFixture<QrcodeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ QrcodeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(QrcodeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchFormComponent } from './search-form.component';
describe('SearchFormComponent', () => {
let component: SearchFormComponent;
let fixture: ComponentFixture<SearchFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchFormComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StartComponent } from './start.component';
describe('StartComponent', () => {
let component: StartComponent;
let fixture: ComponentFixture<StartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StartComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -66,9 +66,9 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<a class="shortable-address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
@@ -164,9 +164,9 @@
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
}">
<td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<a class="shortable-address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>

View File

@@ -6017,6 +6017,20 @@ export const faqData = [
fragment: "what-are-mining-pools",
title: "What are mining pools?",
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-are-vb-wu",
title: "What are virtual bytes (vB) and weight units (WU)?",
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-svb",
title: "What is sat/vB?",
},
{
type: "endpoint",
category: "basics",

View File

@@ -17,15 +17,15 @@ export class ApiDocsNavComponent implements OnInit {
constructor() { }
ngOnInit(): void {
if( this.whichTab === 'rest' ) {
if (this.whichTab === 'rest') {
this.tabData = restApiDocsData;
} else if( this.whichTab = 'faq' ) {
} else if (this.whichTab === 'faq') {
this.tabData = faqData;
}
}
navLinkClick( event ) {
this.navLinkClickEvent.emit( event );
navLinkClick(event) {
this.navLinkClickEvent.emit(event);
}
}

View File

@@ -134,6 +134,19 @@
Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks.
</ng-template>
<ng-template type="what-are-vb-wu">
<p>Virtual bytes (vB) and weight units (WU) are used to measure the size of transactions and blocks on the Bitcoin network.</p>
<p>A Bitcoin transaction's size in the blockchain is <i>not</i> determined how much bitcoin it transfers—instead, a transaction's size is determined by technical factors such as how many inputs and outputs it has, how many signatures it has, and the format it uses (legacy, SegWit, etc). Since space in the Bitcoin blockchain is limited, bigger transactions pay more in mining fees than smaller transactions.</p>
<p>Block sizes are limited to 4,000,000 WU (or 1,000,000 vB since 1 vB = 4 WU).</p>
<p>Transaction sizes and block sizes used to be measured in plain bytes, but virtual bytes and weight units were devised to maintain backward compatibility after the SegWit upgrade in 2017. See <a href="https://programmingbitcoin.com/understanding-segwit-block-size" target="_blank">this post</a> for more details.</p>
</ng-template>
<ng-template type="what-is-svb">
<p>The priority of a pending Bitcoin transaction is determined by its feerate. Feerates are measured in sat/vB.</p>
<p>Using a higher sat/vB feerate for a Bitcoin transaction will generally result in quicker confirmation than using a lower feerate. But feerates change all the time, so it's important to check suggested feerates right before making a transaction to <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="why-is-transaction-stuck-in-mempool">avoid it from getting stuck</a>.</p>
<p>There are feerate estimates on the top of <a [routerLink]="['/' | relativeUrl]">the main dashboard</a> you can use as a guide. See <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="looking-up-fee-estimates">this FAQ</a> for more on picking the right feerate.</p>
</ng-template>
<ng-template type="what-is-full-mempool">
<p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>The default maximum size of a Bitcoin node's mempool is 300MB, so when there are 300MB of transactions in the mempool, we say it's "full".</p>
</ng-template>

View File

@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FiatComponent } from './fiat.component';
describe('FiatComponent', () => {
let component: FiatComponent;
let fixture: ComponentFixture<FiatComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FiatComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FiatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -22,6 +22,7 @@ import { DashboardComponent } from '../dashboard/dashboard.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component';
import { CommonModule } from '@angular/common';
@NgModule({
@@ -47,6 +48,7 @@ import { CommonModule } from '@angular/common';
LbtcPegsGraphComponent,
HashrateChartComponent,
HashrateChartPoolsComponent,
BlockPredictionsGraphComponent,
],
imports: [
CommonModule,

View File

@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component';
import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component';
import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component';
@@ -92,6 +93,10 @@ const routes: Routes = [
path: '',
redirectTo: 'mempool',
},
{
path: 'mining/block-predictions',
component: BlockPredictionsGraphComponent,
},
]
},
{

View File

@@ -140,7 +140,7 @@ export class ApiService {
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
}
getPoolStats$(slug: string): Observable<PoolStat> {
return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`);
@@ -172,6 +172,13 @@ export class ApiService {
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
}
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
@@ -214,6 +221,13 @@ export class ApiService {
);
}
getHistoricalBlockPrediction$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/predictions` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
}

View File

@@ -1,8 +0,0 @@
import { AbsolutePipe } from './absolute.pipe';
describe('AbsolutePipe', () => {
it('create an instance', () => {
const pipe = new AbsolutePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -1,8 +0,0 @@
import { AsmStylerPipe } from './asm-styler.pipe';
describe('OpcodesStylerPipe', () => {
it('create an instance', () => {
const pipe = new AsmStylerPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -1,8 +0,0 @@
import { FeeRoundingPipe } from './fee-rounding.pipe';
describe('FeeRoundingPipe', () => {
it('create an instance', () => {
const pipe = new FeeRoundingPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -1,8 +0,0 @@
import { Hex2asciiPipe } from './hex2ascii.pipe';
describe('Hex2asciiPipe', () => {
it('create an instance', () => {
const pipe = new Hex2asciiPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -1,8 +0,0 @@
import { RelativeUrlPipe } from './relative-url.pipe';
describe('RelativeUrlPipe', () => {
it('create an instance', () => {
const pipe = new RelativeUrlPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -852,6 +852,40 @@ th {
}
}
.fee-progress-bar {
@extend .fee-progress-bar;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;
}
width: 75%;
border-radius: 10px 0px 0px 10px !important;
}
}
.fees-wrapper-tooltip-chart {
@extend .fees-wrapper-tooltip-chart;
.title {
direction: rtl;
}
}
.btn-link {
padding: 0.1rem 0.5rem 0.25rem 0 !important;
}
.shortable-address {
direction: ltr;
}
.lastest-blocks-table {
@extend .lastest-blocks-table;
.table-cell-mined {
@extend .table-cell-mined;
text-align: right !important;
}
}
.mempool-graph {
@extend .mempool-graph;
direction: ltr;

View File

@@ -1,22 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
}
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -1,158 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"forin": false,
"arrow-parens": false,
"arrow-return-shorthand": true,
"curly": true,
"no-bitwise": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"eofline": true,
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"object-literal-shorthand": false,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
false,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
, "variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
}
},
"rulesDirectory": [
"codelyzer"
]
}