2021-07-10 10:04:15 -03:00
|
|
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
|
2021-07-20 09:04:53 -03:00
|
|
|
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs';
|
2020-02-16 22:15:07 +07:00
|
|
|
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
|
|
|
|
import { StateService } from 'src/app/services/state.service';
|
2020-03-21 03:38:18 +07:00
|
|
|
import { Router } from '@angular/router';
|
2021-08-08 21:43:03 -03:00
|
|
|
import { take, map, switchMap, share } from 'rxjs/operators';
|
2020-05-24 22:23:56 +07:00
|
|
|
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
|
2021-11-11 15:49:47 -03:00
|
|
|
import { specialBlocks } from 'src/app/app.constants';
|
|
|
|
import { Block } from 'src/app/interfaces/electrs.interface';
|
2020-05-09 20:37:50 +07:00
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
@Component({
|
|
|
|
selector: 'app-mempool-blocks',
|
|
|
|
templateUrl: './mempool-blocks.component.html',
|
2020-05-24 22:23:56 +07:00
|
|
|
styleUrls: ['./mempool-blocks.component.scss'],
|
2020-07-30 17:01:13 +07:00
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
2020-02-16 22:15:07 +07:00
|
|
|
})
|
2020-03-22 17:44:36 +07:00
|
|
|
export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
2021-11-11 15:49:47 -03:00
|
|
|
specialBlocks = specialBlocks;
|
2021-08-08 21:43:03 -03:00
|
|
|
mempoolBlocks: MempoolBlock[] = [];
|
|
|
|
mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks();
|
2020-07-30 17:01:13 +07:00
|
|
|
mempoolBlocks$: Observable<MempoolBlock[]>;
|
2021-07-20 09:04:53 -03:00
|
|
|
timeAvg$: Observable<number>;
|
2021-08-08 21:43:03 -03:00
|
|
|
loadingBlocks$: Observable<boolean>;
|
2021-11-11 15:49:47 -03:00
|
|
|
blocksSubscription: Subscription;
|
2020-07-30 17:01:13 +07:00
|
|
|
|
2021-08-08 21:43:03 -03:00
|
|
|
mempoolBlocksFull: MempoolBlock[] = [];
|
2020-05-24 22:23:56 +07:00
|
|
|
mempoolBlockStyles = [];
|
2021-08-08 21:43:03 -03:00
|
|
|
mempoolEmptyBlockStyles = [];
|
2020-07-30 17:01:13 +07:00
|
|
|
markBlocksSubscription: Subscription;
|
2021-08-06 08:09:47 -03:00
|
|
|
isLoadingWebsocketSubscription: Subscription;
|
2020-07-30 17:01:13 +07:00
|
|
|
blockSubscription: Subscription;
|
|
|
|
networkSubscription: Subscription;
|
2020-05-09 20:37:50 +07:00
|
|
|
network = '';
|
2021-07-20 09:04:53 -03:00
|
|
|
now = new Date().getTime();
|
2020-02-16 22:15:07 +07:00
|
|
|
|
|
|
|
blockWidth = 125;
|
2020-02-23 05:23:24 +07:00
|
|
|
blockPadding = 30;
|
|
|
|
arrowVisible = false;
|
2020-07-30 17:01:13 +07:00
|
|
|
tabHidden = false;
|
2021-08-07 04:24:46 +03:00
|
|
|
feeRounding = '1.0-0';
|
2020-02-23 05:23:24 +07:00
|
|
|
|
|
|
|
rightPosition = 0;
|
2020-06-10 23:52:14 +07:00
|
|
|
transition = '2s';
|
2020-02-16 22:15:07 +07:00
|
|
|
|
2020-03-22 17:44:36 +07:00
|
|
|
markIndex: number;
|
|
|
|
txFeePerVSize: number;
|
2020-02-16 22:15:07 +07:00
|
|
|
|
2020-03-25 03:59:30 +07:00
|
|
|
resetTransitionTimeout: number;
|
2020-06-10 23:52:14 +07:00
|
|
|
|
|
|
|
blockIndex = 1;
|
2020-05-12 01:12:38 +07:00
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
constructor(
|
2020-03-21 03:38:18 +07:00
|
|
|
private router: Router,
|
2021-07-31 17:30:35 +03:00
|
|
|
public stateService: StateService,
|
2020-07-30 17:01:13 +07:00
|
|
|
private cd: ChangeDetectorRef,
|
2020-02-16 22:15:07 +07:00
|
|
|
) { }
|
|
|
|
|
|
|
|
ngOnInit() {
|
2021-08-07 04:24:46 +03:00
|
|
|
if (this.stateService.network === 'liquid') {
|
|
|
|
this.feeRounding = '1.0-1';
|
|
|
|
}
|
2021-08-08 21:43:03 -03:00
|
|
|
this.mempoolEmptyBlocks.forEach((b) => {
|
|
|
|
this.mempoolEmptyBlockStyles.push(this.getStyleForMempoolEmptyBlock(b.index));
|
|
|
|
});
|
|
|
|
this.reduceMempoolBlocksToFitScreen(this.mempoolEmptyBlocks);
|
|
|
|
|
2021-07-10 10:04:15 -03:00
|
|
|
this.mempoolBlocks.map(() => {
|
|
|
|
this.updateMempoolBlockStyles();
|
|
|
|
this.calculateTransactionPosition();
|
|
|
|
});
|
|
|
|
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
2020-06-11 01:38:59 +07:00
|
|
|
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
2021-08-08 21:43:03 -03:00
|
|
|
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
2020-06-11 01:38:59 +07:00
|
|
|
|
2020-07-30 17:01:13 +07:00
|
|
|
this.mempoolBlocks$ = merge(
|
|
|
|
of(true),
|
|
|
|
fromEvent(window, 'resize')
|
|
|
|
)
|
|
|
|
.pipe(
|
2021-11-11 15:49:47 -03:00
|
|
|
switchMap(() => combineLatest([
|
|
|
|
this.stateService.blocks$.pipe(map(([block]) => block)),
|
|
|
|
this.stateService.mempoolBlocks$
|
|
|
|
])),
|
|
|
|
map(([lastBlock, mempoolBlocks]) => {
|
|
|
|
mempoolBlocks.forEach((block, i) => {
|
|
|
|
block.index = this.blockIndex + i;
|
|
|
|
block.height = lastBlock.height + i + 1;
|
|
|
|
block.blink = specialBlocks[block.height] ? true : false;
|
|
|
|
});
|
|
|
|
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
|
|
|
this.mempoolBlocksFull = JSON.parse(stringifiedBlocks);
|
|
|
|
this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks));
|
|
|
|
this.updateMempoolBlockStyles();
|
|
|
|
this.calculateTransactionPosition();
|
|
|
|
|
|
|
|
return this.mempoolBlocks;
|
|
|
|
})
|
|
|
|
);
|
2021-07-20 09:04:53 -03:00
|
|
|
|
|
|
|
this.timeAvg$ = timer(0, 1000)
|
|
|
|
.pipe(
|
|
|
|
switchMap(() => combineLatest([
|
|
|
|
this.stateService.blocks$.pipe(map(([block]) => block)),
|
|
|
|
this.stateService.lastDifficultyAdjustment$
|
|
|
|
])),
|
|
|
|
map(([block, DATime]) => {
|
2021-07-24 19:26:29 -03:00
|
|
|
this.now = new Date().getTime();
|
2021-07-20 09:04:53 -03:00
|
|
|
const now = new Date().getTime() / 1000;
|
|
|
|
const diff = now - DATime;
|
|
|
|
const blocksInEpoch = block.height % 2016;
|
|
|
|
let difficultyChange = 0;
|
|
|
|
if (blocksInEpoch > 0) {
|
|
|
|
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
|
|
|
|
}
|
|
|
|
const timeAvgDiff = difficultyChange * 0.1;
|
|
|
|
|
|
|
|
let timeAvgMins = 10;
|
|
|
|
if (timeAvgDiff > 0 ){
|
|
|
|
timeAvgMins -= Math.abs(timeAvgDiff);
|
|
|
|
} else {
|
|
|
|
timeAvgMins += Math.abs(timeAvgDiff);
|
|
|
|
}
|
2021-11-11 15:49:47 -03:00
|
|
|
|
2021-07-20 09:04:53 -03:00
|
|
|
return timeAvgMins * 60 * 1000;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2020-07-30 17:01:13 +07:00
|
|
|
this.markBlocksSubscription = this.stateService.markBlock$
|
2020-03-22 17:44:36 +07:00
|
|
|
.subscribe((state) => {
|
|
|
|
this.markIndex = undefined;
|
|
|
|
this.txFeePerVSize = undefined;
|
|
|
|
if (state.mempoolBlockIndex !== undefined) {
|
|
|
|
this.markIndex = state.mempoolBlockIndex;
|
|
|
|
}
|
|
|
|
if (state.txFeePerVSize) {
|
|
|
|
this.txFeePerVSize = state.txFeePerVSize;
|
|
|
|
}
|
|
|
|
this.calculateTransactionPosition();
|
2020-07-30 17:01:13 +07:00
|
|
|
this.cd.markForCheck();
|
2020-03-22 17:44:36 +07:00
|
|
|
});
|
2020-05-09 20:37:50 +07:00
|
|
|
|
2020-07-30 17:01:13 +07:00
|
|
|
this.blockSubscription = this.stateService.blocks$
|
2020-06-10 23:52:14 +07:00
|
|
|
.subscribe(([block]) => {
|
2020-07-22 19:21:40 +07:00
|
|
|
if (block.matchRate >= 66 && !this.tabHidden) {
|
2020-06-10 23:52:14 +07:00
|
|
|
this.blockIndex++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-07-30 17:01:13 +07:00
|
|
|
this.networkSubscription = this.stateService.networkChanged$
|
2020-05-09 20:37:50 +07:00
|
|
|
.subscribe((network) => this.network = network);
|
2020-03-12 21:56:07 +07:00
|
|
|
|
2020-05-13 13:03:57 +07:00
|
|
|
this.stateService.keyNavigation$.subscribe((event) => {
|
2020-03-22 17:44:36 +07:00
|
|
|
if (this.markIndex === undefined) {
|
|
|
|
return;
|
2020-03-21 03:38:18 +07:00
|
|
|
}
|
2020-05-13 13:03:57 +07:00
|
|
|
|
2020-03-22 17:44:36 +07:00
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
if (this.mempoolBlocks[this.markIndex - 1]) {
|
2020-05-10 01:34:28 +07:00
|
|
|
this.router.navigate([(this.network ? '/' + this.network : '') + '/mempool-block/', this.markIndex - 1]);
|
2020-03-22 17:44:36 +07:00
|
|
|
} else {
|
|
|
|
this.stateService.blocks$
|
2021-07-31 17:56:10 +03:00
|
|
|
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
|
2020-06-10 23:52:14 +07:00
|
|
|
.subscribe(([block]) => {
|
2020-03-22 17:44:36 +07:00
|
|
|
if (this.stateService.latestBlockHeight === block.height) {
|
2020-05-10 01:34:28 +07:00
|
|
|
this.router.navigate([(this.network ? '/' + this.network : '') + '/block/', block.id], { state: { data: { block } }});
|
2020-03-22 17:44:36 +07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (event.key === 'ArrowLeft') {
|
|
|
|
if (this.mempoolBlocks[this.markIndex + 1]) {
|
2020-05-10 01:34:28 +07:00
|
|
|
this.router.navigate([(this.network ? '/' + this.network : '') + '/mempool-block/', this.markIndex + 1]);
|
2020-03-22 17:44:36 +07:00
|
|
|
}
|
2020-03-21 03:38:18 +07:00
|
|
|
}
|
2020-03-22 17:44:36 +07:00
|
|
|
});
|
2020-02-23 05:23:24 +07:00
|
|
|
}
|
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
ngOnDestroy() {
|
2020-07-30 17:01:13 +07:00
|
|
|
this.markBlocksSubscription.unsubscribe();
|
|
|
|
this.blockSubscription.unsubscribe();
|
|
|
|
this.networkSubscription.unsubscribe();
|
|
|
|
clearTimeout(this.resetTransitionTimeout);
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
|
|
|
|
2020-06-10 23:52:14 +07:00
|
|
|
trackByFn(index: number, block: MempoolBlock) {
|
|
|
|
return block.index;
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|
|
|
|
|
2020-03-12 21:56:07 +07:00
|
|
|
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
2021-10-05 15:40:28 +04:00
|
|
|
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
|
|
|
const blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
2020-03-12 21:56:07 +07:00
|
|
|
while (blocks.length > blocksAmount) {
|
|
|
|
const block = blocks.pop();
|
|
|
|
const lastBlock = blocks[blocks.length - 1];
|
|
|
|
lastBlock.blockSize += block.blockSize;
|
|
|
|
lastBlock.blockVSize += block.blockVSize;
|
|
|
|
lastBlock.nTx += block.nTx;
|
|
|
|
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
|
|
|
|
lastBlock.feeRange.sort((a, b) => a - b);
|
|
|
|
lastBlock.medianFee = this.median(lastBlock.feeRange);
|
|
|
|
}
|
|
|
|
return blocks;
|
|
|
|
}
|
|
|
|
|
|
|
|
median(numbers: number[]) {
|
|
|
|
let medianNr = 0;
|
|
|
|
const numsLen = numbers.length;
|
|
|
|
if (numsLen % 2 === 0) {
|
|
|
|
medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
|
|
|
|
} else {
|
|
|
|
medianNr = numbers[(numsLen - 1) / 2];
|
|
|
|
}
|
|
|
|
return medianNr;
|
|
|
|
}
|
|
|
|
|
2020-05-24 22:23:56 +07:00
|
|
|
updateMempoolBlockStyles() {
|
|
|
|
this.mempoolBlockStyles = [];
|
|
|
|
this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i)));
|
|
|
|
}
|
|
|
|
|
|
|
|
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
|
2021-07-31 17:30:35 +03:00
|
|
|
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
|
2020-05-31 18:34:03 +07:00
|
|
|
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
|
2020-05-24 22:23:56 +07:00
|
|
|
const backgroundGradients = [`repeating-linear-gradient(to right, #554b45, #554b45 ${emptyBackgroundSpacePercentage}%`];
|
|
|
|
const gradientColors = [];
|
|
|
|
|
|
|
|
const trimmedFeeRange = index === 0 ? mempoolBlock.feeRange.slice(0, -1) : mempoolBlock.feeRange;
|
|
|
|
|
|
|
|
trimmedFeeRange.forEach((fee: number) => {
|
|
|
|
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fee >= feeLvl);
|
|
|
|
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
|
|
|
|
gradientColors.push(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
|
|
|
});
|
|
|
|
|
2020-05-31 18:34:03 +07:00
|
|
|
|
2020-05-24 22:23:56 +07:00
|
|
|
gradientColors.forEach((color, i, gc) => {
|
|
|
|
backgroundGradients.push(`
|
2020-05-31 18:34:03 +07:00
|
|
|
#${i === 0 ? color : gc[i - 1]} ${ i === 0 ? emptyBackgroundSpacePercentage : ((i / gradientColors.length) * 100) * usedBlockSpace / 100 + emptyBackgroundSpacePercentage }%,
|
|
|
|
#${color} ${Math.round(((i + 1) / gradientColors.length) * 100) * usedBlockSpace / 100 + emptyBackgroundSpacePercentage}%
|
2020-05-24 22:23:56 +07:00
|
|
|
`);
|
|
|
|
});
|
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
return {
|
|
|
|
'right': 40 + index * 155 + 'px',
|
2020-05-24 22:23:56 +07:00
|
|
|
'background': backgroundGradients.join(',') + ')'
|
2020-02-16 22:15:07 +07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-08-08 21:43:03 -03:00
|
|
|
getStyleForMempoolEmptyBlock(index: number) {
|
|
|
|
return {
|
|
|
|
'right': 40 + index * 155 + 'px',
|
|
|
|
'background': '#554b45',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
calculateTransactionPosition() {
|
2020-03-20 02:11:40 +07:00
|
|
|
if ((!this.txFeePerVSize && (this.markIndex === undefined || this.markIndex === -1)) || !this.mempoolBlocks) {
|
2020-02-23 05:23:24 +07:00
|
|
|
this.arrowVisible = false;
|
2020-02-16 22:15:07 +07:00
|
|
|
return;
|
2020-03-20 02:11:40 +07:00
|
|
|
} else if (this.markIndex > -1) {
|
2020-03-25 03:59:30 +07:00
|
|
|
clearTimeout(this.resetTransitionTimeout);
|
2020-03-21 03:38:18 +07:00
|
|
|
this.transition = 'inherit';
|
2020-03-17 21:53:20 +07:00
|
|
|
this.rightPosition = this.markIndex * (this.blockWidth + this.blockPadding) + 0.5 * this.blockWidth;
|
2020-03-20 02:11:40 +07:00
|
|
|
this.arrowVisible = true;
|
2020-07-30 17:01:13 +07:00
|
|
|
|
|
|
|
this.resetTransitionTimeout = window.setTimeout(() => {
|
|
|
|
this.transition = '2s';
|
|
|
|
this.cd.markForCheck();
|
|
|
|
}, 100);
|
2020-03-17 21:53:20 +07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-20 02:11:40 +07:00
|
|
|
this.arrowVisible = true;
|
|
|
|
|
2020-02-16 22:15:07 +07:00
|
|
|
for (const block of this.mempoolBlocks) {
|
|
|
|
for (let i = 0; i < block.feeRange.length - 1; i++) {
|
|
|
|
if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
|
|
|
|
const txInBlockIndex = this.mempoolBlocks.indexOf(block);
|
|
|
|
const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]);
|
|
|
|
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
|
|
|
|
|
|
|
|
const txFee = this.txFeePerVSize - block.feeRange[i];
|
|
|
|
const max = block.feeRange[i + 1] - block.feeRange[i];
|
|
|
|
const blockLocation = txFee / max;
|
|
|
|
|
|
|
|
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
|
|
|
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
|
|
|
|
2021-07-31 17:30:35 +03:00
|
|
|
const blockedFilledPercentage = (block.blockVSize > this.stateService.blockVSize ? this.stateService.blockVSize : block.blockVSize) / this.stateService.blockVSize;
|
2020-02-23 05:23:24 +07:00
|
|
|
const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding)
|
2020-02-16 22:15:07 +07:00
|
|
|
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
|
|
|
|
2020-02-23 05:23:24 +07:00
|
|
|
this.rightPosition = arrowRightPosition;
|
2020-02-16 22:15:07 +07:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-10 10:04:15 -03:00
|
|
|
mountEmptyBlocks() {
|
|
|
|
const emptyBlocks = [];
|
2021-07-31 17:56:10 +03:00
|
|
|
const numberOfBlocks = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
|
|
|
|
for (let i = 0; i < numberOfBlocks; i++) {
|
2021-07-10 10:04:15 -03:00
|
|
|
emptyBlocks.push({
|
|
|
|
blockSize: 0,
|
|
|
|
blockVSize: 0,
|
|
|
|
feeRange: [],
|
2021-08-08 21:43:03 -03:00
|
|
|
index: i,
|
2021-07-10 10:04:15 -03:00
|
|
|
medianFee: 0,
|
|
|
|
nTx: 0,
|
|
|
|
totalFees: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return emptyBlocks;
|
|
|
|
}
|
2020-02-16 22:15:07 +07:00
|
|
|
}
|