Merge branch 'master' into mononaut/full-rbf-highlight

This commit is contained in:
wiz
2023-07-13 14:16:33 +09:00
committed by GitHub
23 changed files with 222 additions and 122 deletions

View File

@@ -14,6 +14,7 @@ import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-block',
@@ -72,6 +73,7 @@ export class BlockComponent implements OnInit, OnDestroy {
auditSubscription: Subscription;
keyNavigationSubscription: Subscription;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
nextBlockSubscription: Subscription = undefined;
@@ -99,6 +101,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService,
private priceService: PriceService,
private cacheService: CacheService,
) {
this.webGlEnabled = detectWebGL();
}
@@ -128,19 +131,27 @@ export class BlockComponent implements OnInit, OnDestroy {
map((indicators) => indicators['blocktxs-' + this.blockHash] !== undefined ? indicators['blocktxs-' + this.blockHash] : 0)
);
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
this.loadedCacheBlock(block);
});
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
this.latestBlock = block;
this.latestBlocks.unshift(block);
this.latestBlocks = this.latestBlocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
.subscribe((blocks) => {
this.latestBlock = blocks[0];
this.latestBlocks = blocks;
this.setNextAndPreviousBlockLink();
if (block.id === this.blockHash) {
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
for (const block of blocks) {
if (block.id === this.blockHash) {
this.block = block;
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
}
} else if (block.height === this.block?.height) {
this.block.stale = true;
this.block.canonical = block.id;
}
}
});
@@ -254,6 +265,13 @@ export class BlockComponent implements OnInit, OnDestroy {
this.transactionsError = null;
this.isLoadingOverview = true;
this.overviewError = null;
const cachedBlock = this.cacheService.getCachedBlock(block.height);
if (!cachedBlock) {
this.cacheService.loadBlock(block.height);
} else {
this.loadedCacheBlock(cachedBlock);
}
}),
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
shareReplay(1)
@@ -459,6 +477,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.auditSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
this.blocksSubscription?.unsubscribe();
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
this.timeLtrSubscription?.unsubscribe();
@@ -679,4 +698,11 @@ export class BlockComponent implements OnInit, OnDestroy {
}
return 0;
}
loadedCacheBlock(block: BlockExtended): void {
if (this.block && block.height === this.block.height && block.id !== this.block.id) {
this.block.stale = true;
this.block.canonical = block.id;
}
}
}

View File

@@ -36,11 +36,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
markHeight: number;
chainTip: number;
pendingMarkBlock: { animate: boolean, newBlockFromLeft: boolean };
blocksSubscription: Subscription;
blockPageSubscription: Subscription;
networkSubscription: Subscription;
tabHiddenSubscription: Subscription;
markBlockSubscription: Subscription;
txConfirmedSubscription: Subscription;
loadingBlocks$: Observable<boolean>;
blockStyles = [];
emptyBlockStyles = [];
@@ -82,7 +84,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
ngOnInit() {
this.chainTip = this.stateService.latestBlockHeight;
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
@@ -104,31 +105,22 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
if (!this.static) {
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block, txConfirmed]) => {
if (this.blocks.some((b) => b.height === block.height)) {
.subscribe((blocks) => {
if (!blocks?.length) {
return;
}
const latestHeight = blocks[0].height;
const animate = this.chainTip != null && latestHeight > this.chainTip;
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
this.blocks = [];
this.blocksFilled = false;
for (const block of blocks) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
}
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
if (txConfirmed && block.height > this.chainTip) {
this.markHeight = block.height;
this.moveArrowToPosition(true, true);
} else {
this.moveArrowToPosition(true, false);
}
this.blocks = blocks;
this.blockStyles = [];
if (this.blocksFilled && block.height > this.chainTip) {
if (animate) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset)));
setTimeout(() => {
this.blockStyles = [];
@@ -139,13 +131,23 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
}
if (this.blocks.length === this.dynamicBlocksAmount) {
this.blocksFilled = true;
}
this.chainTip = latestHeight;
this.chainTip = Math.max(this.chainTip, block.height);
if (this.pendingMarkBlock) {
this.moveArrowToPosition(this.pendingMarkBlock.animate, this.pendingMarkBlock.newBlockFromLeft);
this.pendingMarkBlock = null;
}
this.cd.markForCheck();
});
this.txConfirmedSubscription = this.stateService.txConfirmed$.subscribe(([txid, block]) => {
if (txid) {
this.markHeight = block.height;
this.moveArrowToPosition(true, true);
} else {
this.moveArrowToPosition(true, false);
}
})
} else {
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
if (block.height <= this.height && block.height > this.height - this.count) {
@@ -164,9 +166,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck();
});
if (this.static) {
this.updateStaticBlocks();
}
if (this.static) {
this.updateStaticBlocks();
}
}
ngOnChanges(changes: SimpleChanges): void {
@@ -190,6 +192,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.blockPageSubscription) {
this.blockPageSubscription.unsubscribe();
}
if (this.txConfirmedSubscription) {
this.txConfirmedSubscription.unsubscribe();
}
this.networkSubscription.unsubscribe();
this.tabHiddenSubscription.unsubscribe();
this.markBlockSubscription.unsubscribe();
@@ -202,6 +207,9 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.arrowVisible = false;
return;
}
if (this.chainTip == null) {
this.pendingMarkBlock = { animate, newBlockFromLeft };
}
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
if (blockindex > -1) {
if (!animate) {

View File

@@ -82,12 +82,12 @@ export class BlocksList implements OnInit {
),
this.stateService.blocks$
.pipe(
switchMap((block) => {
if (block[0].height <= this.lastBlockHeight) {
switchMap((blocks) => {
if (blocks[0].height <= this.lastBlockHeight) {
return [null]; // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = block[0].height;
return [block];
this.lastBlockHeight = blocks[0].height;
return blocks;
})
)
])

View File

@@ -39,13 +39,10 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
})
).subscribe();
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
// using block-reported times, so ensure they are sorted chronologically
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
}
.subscribe((blocks) => {
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
});
}

View File

@@ -60,14 +60,11 @@ export class ClockComponent implements OnInit {
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 16);
if (this.blocks[this.blockIndex]) {
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
this.cd.markForCheck();
}
.subscribe((blocks) => {
this.blocks = blocks.slice(0, 16);
if (this.blocks[this.blockIndex]) {
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
this.cd.markForCheck();
}
});

View File

@@ -38,11 +38,12 @@ export class DifficultyMiningComponent implements OnInit {
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.blocks$,
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
map(([blocks, da]) => {
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
@@ -63,7 +64,7 @@ export class DifficultyMiningComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const blocksUntilHalving = 210000 - (maxHeight % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const data = {

View File

@@ -67,11 +67,12 @@ export class DifficultyComponent implements OnInit {
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.difficultyEpoch$ = combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.blocks$,
this.stateService.difficultyAdjustment$,
])
.pipe(
map(([block, da]) => {
map(([blocks, da]) => {
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
let colorAdjustments = '#ffffff66';
if (da.difficultyChange > 0) {
colorAdjustments = '#3bcc49';
@@ -92,7 +93,7 @@ export class DifficultyComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66';
}
const blocksUntilHalving = 210000 - (block.height % 210000);
const blocksUntilHalving = 210000 - (maxHeight % 210000);
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);

View File

@@ -124,7 +124,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
)
.pipe(
switchMap(() => combineLatest([
this.stateService.blocks$.pipe(map(([block]) => block)),
this.stateService.blocks$.pipe(map((blocks) => blocks[0])),
this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
@@ -186,8 +186,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.cd.markForCheck();
});
this.blockSubscription = this.stateService.blocks$
.subscribe(([block]) => {
this.blockSubscription = this.stateService.blocks$.pipe(map((blocks) => blocks[0]))
.subscribe((block) => {
if (!block) {
return;
}
if (this.chainTip === -1) {
this.animateEntry = block.height === this.stateService.latestBlockHeight;
} else {
@@ -221,8 +224,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
} else {
this.stateService.blocks$
.pipe(take(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT))
.subscribe(([block]) => {
.pipe(map((blocks) => blocks[0]))
.subscribe((block) => {
if (this.stateService.latestBlockHeight === block.height) {
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
}
@@ -297,7 +300,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
while (blocks.length > blocksAmount) {
const block = blocks.pop();
if (!this.count) {
const lastBlock = blocks[blocks.length - 1];
const lastBlock = blocks[0];
lastBlock.blockSize += block.blockSize;
lastBlock.blockVSize += block.blockVSize;
lastBlock.nTx += block.nTx;
@@ -308,7 +311,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
}
if (blocks.length) {
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
}
return blocks;
}

View File

@@ -37,7 +37,7 @@ export class PoolComponent implements OnInit {
auditAvailable = false;
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[0]?.height);
constructor(
@Inject(LOCALE_ID) public locale: string,
@@ -68,7 +68,7 @@ export class PoolComponent implements OnInit {
return this.apiService.getPoolStats$(slug);
}),
tap(() => {
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
this.loadMoreSubject.next(this.blocks[0]?.height);
}),
map((poolStats) => {
this.seoService.setTitle(poolStats.pool.name);
@@ -91,7 +91,7 @@ export class PoolComponent implements OnInit {
if (this.slug === undefined) {
return [];
}
return this.apiService.getPoolBlocks$(this.slug, this.blocks[this.blocks.length - 1]?.height);
return this.apiService.getPoolBlocks$(this.slug, this.blocks[0]?.height);
}),
tap((newBlocks) => {
this.blocks = this.blocks.concat(newBlocks);
@@ -237,7 +237,7 @@ export class PoolComponent implements OnInit {
}
loadMore() {
this.loadMoreSubject.next(this.blocks[this.blocks.length - 1]?.height);
this.loadMoreSubject.next(this.blocks[0]?.height);
}
trackByBlock(index: number, block: BlockExtended) {

View File

@@ -29,11 +29,12 @@ export class RewardStatsComponent implements OnInit {
// Or when we receive a newer block, newer than the latest reward stats api call
this.stateService.blocks$
.pipe(
switchMap((block) => {
if (block[0].height <= this.lastBlockHeight) {
switchMap((blocks) => {
const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), 0);
if (maxHeight <= this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = block[0].height;
this.lastBlockHeight = maxHeight;
return this.apiService.getRewardStats$();
})
)

View File

@@ -2,6 +2,7 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Inpu
import { Subscription } from 'rxjs';
import { MarkBlockState, StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-start',
@@ -55,8 +56,8 @@ export class StartComponent implements OnInit, OnDestroy {
ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => {
this.blockCount++;
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.blockCount = blocks.length;
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
@@ -110,9 +111,12 @@ export class StartComponent implements OnInit, OnDestroy {
}
});
this.stateService.blocks$
.subscribe((blocks: any) => {
.subscribe((blocks: BlockExtended[]) => {
this.countdown = 0;
const block = blocks[0];
if (!block) {
return;
}
for (const sb in specialBlocks) {
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {

View File

@@ -306,7 +306,7 @@
</ng-template>
<ng-template [ngIf]="isLoadingTx && !error">
<ng-template [ngIf]="(isLoadingTx && !error) || loadingCachedTx">
<div class="box">
<div class="row">
@@ -451,7 +451,7 @@
</ng-template>
<ng-template [ngIf]="error">
<ng-template [ngIf]="error && !loadingCachedTx">
<div class="text-center" *ngIf="waitingForTransaction; else errorTemplate">
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>

View File

@@ -12,7 +12,7 @@ import {
tap
} from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs';
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service';
@@ -39,6 +39,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
isLoadingTx = true;
error: any = undefined;
errorUnblinded: any = undefined;
loadingCachedTx = false;
waitingForTransaction = false;
latestBlock: BlockExtended;
transactionTime = -1;
@@ -49,10 +50,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txReplacedSubscription: Subscription;
txRbfInfoSubscription: Subscription;
mempoolPositionSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
mempoolBlocksSubscription: Subscription;
blocksSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
@@ -131,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
});
this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.latestBlock = blocks[0];
});
this.fetchCpfpSubscription = this.fetchCpfp$
.pipe(
switchMap((txId) =>
@@ -199,6 +204,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCachedTxSubscription = this.fetchCachedTx$
.pipe(
tap(() => {
this.loadingCachedTx = true;
}),
switchMap((txId) =>
this.apiService
.getRbfCachedTx$(txId)
@@ -207,6 +215,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(null);
})
).subscribe((tx) => {
this.loadingCachedTx = false;
if (!tx) {
return;
}
@@ -338,6 +347,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.loadingCachedTx = false;
this.waitingForTransaction = false;
this.websocketService.startTrackTransaction(tx.txid);
this.graphExpanded = false;
@@ -391,9 +401,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
);
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
this.tx.status = {
confirmed: true,
@@ -409,6 +417,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
if (!this.tx) {
this.error = new Error();
this.loadingCachedTx = false;
this.waitingForTransaction = false;
}
this.rbfTransaction = rbfTransaction;
@@ -593,13 +602,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCachedTxSubscription.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.txRbfInfoSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.mempoolPositionSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.leaveTransaction();
}
}

View File

@@ -56,7 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
) { }
ngOnInit(): void {
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.networkChanged$.subscribe((network) => this.network = network);
if (this.network === 'liquid' || this.network === 'liquidtestnet') {