diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 4d218ed54..5939421a7 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -25,6 +25,7 @@ import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmen import PricesRepository from '../repositories/PricesRepository'; import priceUpdater from '../tasks/price-updater'; import chainTips from './chain-tips'; +import websocketHandler from './websocket-handler'; class Blocks { private blocks: BlockExtended[] = []; @@ -686,6 +687,8 @@ class Blocks { this.updateTimerProgress(timer, `reindexed difficulty adjustments`); logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining); indexer.reindex(); + + websocketHandler.handleReorg(); } } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index ae536b72e..f91947dcb 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -333,6 +333,40 @@ class WebsocketHandler { }); } + handleReorg(): void { + if (!this.wss) { + throw new Error('WebSocket.Server is not set'); + } + + const da = difficultyAdjustment.getDifficultyAdjustment(); + + // update init data + this.updateSocketDataFields({ + 'blocks': blocks.getBlocks(), + 'da': da?.previousTime ? da : undefined, + }); + + this.wss.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) { + return; + } + + const response = {}; + + if (client['want-blocks']) { + response['blocks'] = this.socketData['blocks']; + } + if (client['want-stats']) { + response['da'] = this.socketData['da']; + } + + if (Object.keys(response).length) { + const serializedResponse = this.serializeResponse(response); + client.send(serializedResponse); + } + }); + } + async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise { if (!this.wss) { diff --git a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts index fb30fc59f..47ac0d6db 100644 --- a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts @@ -112,7 +112,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy { this.error = error; }); - this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block))); + this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0])); this.stateService.bsqPrice$ .subscribe((bsqPrice) => { diff --git a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts index 4346f15d3..a46cbf07f 100644 --- a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts +++ b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts @@ -27,7 +27,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges { } ngOnInit() { - this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); + this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0])); } ngOnChanges() { diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index ad008089d..0d733ff6b 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -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; + } + } } \ No newline at end of file diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 5242c1fe5..245973885 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -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; 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) { diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index 324807628..2b54058e8 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -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; }) ) ]) diff --git a/frontend/src/app/components/clock-face/clock-face.component.ts b/frontend/src/app/components/clock-face/clock-face.component.ts index c2c946b74..63d87c436 100644 --- a/frontend/src/app/components/clock-face/clock-face.component.ts +++ b/frontend/src/app/components/clock-face/clock-face.component.ts @@ -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(); }); } diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index b1a9d2159..7ae38583a 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -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(); } }); diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index fbf31f238..c23d7d4b9 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -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 = { diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index b246a14fe..d3983c939 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -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); diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index bc3633be0..3ec240b78 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -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; } diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 85fd028ef..139da5ef0 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -37,7 +37,7 @@ export class PoolComponent implements OnInit { auditAvailable = false; - loadMoreSubject: BehaviorSubject = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height); + loadMoreSubject: BehaviorSubject = 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) { diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts index 30bf26488..5aac641b0 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.ts +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -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$(); }) ) diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 22d3d6350..33770bb24 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -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')) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 25707b007..203a5df5c 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -306,7 +306,7 @@ - +
@@ -451,7 +451,7 @@ - +

Transaction not found.

diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index bbf679dcf..0faa328c0 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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(); } } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 53ddb449c..c49ff0e3c 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -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') { diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 7e4645fe0..6cf487be6 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -132,26 +132,19 @@ export class DashboardComponent implements OnInit, OnDestroy { this.blocks$ = this.stateService.blocks$ .pipe( - tap(([block]) => { - this.latestBlockHeight = block.height; + tap((blocks) => { + this.latestBlockHeight = blocks[0].height; }), - scan((acc, [block]) => { - if (acc.find((b) => b.height == block.height)) { - return acc; - } - acc.unshift(block); - acc = acc.slice(0, 6); - + switchMap((blocks) => { if (this.stateService.env.MINING_DASHBOARD === true) { - for (const block of acc) { + for (const block of blocks) { // @ts-ignore: Need to add an extra field for the template block.extras.pool.logo = `/resources/mining-pools/` + block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; } } - - return acc; - }, []), + return of(blocks.slice(0, 6)); + }) ); this.transactions$ = this.stateService.transactions$ diff --git a/frontend/src/app/fiat/fiat.component.html b/frontend/src/app/fiat/fiat.component.html index afee30d27..ebf59ffb1 100644 --- a/frontend/src/app/fiat/fiat.component.html +++ b/frontend/src/app/fiat/fiat.component.html @@ -8,7 +8,10 @@ - + {{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }} + +   + \ No newline at end of file diff --git a/frontend/src/app/services/cache.service.ts b/frontend/src/app/services/cache.service.ts index 5eefd6e0a..8c90dc210 100644 --- a/frontend/src/app/services/cache.service.ts +++ b/frontend/src/app/services/cache.service.ts @@ -18,6 +18,7 @@ export class CacheService { txCache: { [txid: string]: Transaction } = {}; network: string; + blockHashCache: { [hash: string]: BlockExtended } = {}; blockCache: { [height: number]: BlockExtended } = {}; blockLoading: { [height: number]: boolean } = {}; copiesInBlockQueue: { [height: number]: number } = {}; @@ -27,8 +28,10 @@ export class CacheService { private stateService: StateService, private apiService: ApiService, ) { - this.stateService.blocks$.subscribe(([block]) => { - this.addBlockToCache(block); + this.stateService.blocks$.subscribe((blocks) => { + for (const block of blocks) { + this.addBlockToCache(block); + } this.clearBlocks(); }); this.stateService.chainTip$.subscribe((height) => { @@ -56,8 +59,11 @@ export class CacheService { } addBlockToCache(block: BlockExtended) { - this.blockCache[block.height] = block; - this.bumpBlockPriority(block.height); + if (!this.blockHashCache[block.id]) { + this.blockHashCache[block.id] = block; + this.blockCache[block.height] = block; + this.bumpBlockPriority(block.height); + } } async loadBlock(height) { @@ -105,7 +111,9 @@ export class CacheService { } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) { this.bumpBlockPriority(height); } else { + const block = this.blockCache[height]; delete this.blockCache[height]; + delete this.blockHashCache[block.id]; delete this.copiesInBlockQueue[height]; } } @@ -113,6 +121,7 @@ export class CacheService { // remove all blocks from the cache resetBlockCache() { + this.blockHashCache = {}; this.blockCache = {}; this.blockLoading = {}; this.copiesInBlockQueue = {}; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index c1b4421df..f38600605 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -5,7 +5,7 @@ import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommended import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; -import { map, scan, shareReplay } from 'rxjs/operators'; +import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; export interface MarkBlockState { @@ -90,10 +90,12 @@ export class StateService { blockVSize: number; env: Env; latestBlockHeight = -1; + blocks: BlockExtended[] = []; networkChanged$ = new ReplaySubject(1); lightningChanged$ = new ReplaySubject(1); - blocks$: ReplaySubject<[BlockExtended, string]>; + blocksSubject$ = new BehaviorSubject([]); + blocks$: Observable; transactions$ = new ReplaySubject(6); conversions$ = new ReplaySubject(1); bsqPrice$ = new ReplaySubject(1); @@ -102,6 +104,7 @@ export class StateService { mempoolBlockTransactions$ = new Subject(); mempoolBlockDelta$ = new Subject(); liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; + txConfirmed$ = new Subject<[string, BlockExtended]>(); txReplaced$ = new Subject(); txRbfInfo$ = new Subject(); rbfLatest$ = new Subject(); @@ -167,8 +170,6 @@ export class StateService { } }); - this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT); - this.liveMempoolBlockTransactions$ = merge( this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })), this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })), @@ -200,11 +201,13 @@ export class StateService { this.networkChanged$.subscribe((network) => { this.transactions$ = new ReplaySubject(6); - this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT); + this.blocksSubject$.next([]); }); this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4; + this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0)); + const savedTimePreference = this.storageService.getValue('time-preference-ltr'); const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')); // default time direction is right-to-left, unless locale is a RTL language @@ -341,4 +344,15 @@ export class StateService { this.chainTip$.next(height); } } + + resetBlocks(blocks: BlockExtended[]): void { + this.blocks = blocks.reverse(); + this.blocksSubject$.next(blocks); + } + + addBlock(block: BlockExtended): void { + this.blocks.unshift(block); + this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT); + this.blocksSubject$.next(this.blocks); + } } diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index d22717b2a..7eed09e77 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface'; +import { WebsocketResponse } from '../interfaces/websocket.interface'; import { StateService } from './state.service'; import { Transaction } from '../interfaces/electrs.interface'; import { Subscription } from 'rxjs'; import { ApiService } from './api.service'; import { take } from 'rxjs/operators'; import { TransferState, makeStateKey } from '@angular/platform-browser'; -import { BlockExtended } from '../interfaces/node-api.interface'; +import { CacheService } from './cache.service'; const OFFLINE_RETRY_AFTER_MS = 2000; const OFFLINE_PING_CHECK_AFTER_MS = 30000; @@ -40,6 +40,7 @@ export class WebsocketService { private stateService: StateService, private apiService: ApiService, private transferState: TransferState, + private cacheService: CacheService, ) { if (!this.stateService.isBrowser) { // @ts-ignore @@ -239,13 +240,8 @@ export class WebsocketService { if (response.blocks && response.blocks.length) { const blocks = response.blocks; - let maxHeight = 0; - blocks.forEach((block: BlockExtended) => { - if (block.height > this.stateService.latestBlockHeight) { - maxHeight = Math.max(maxHeight, block.height); - this.stateService.blocks$.next([block, '']); - } - }); + this.stateService.resetBlocks(blocks); + const maxHeight = blocks.reduce((max, block) => Math.max(max, block.height), this.stateService.latestBlockHeight); this.stateService.updateChainTip(maxHeight); } @@ -260,7 +256,8 @@ export class WebsocketService { if (response.block) { if (response.block.height === this.stateService.latestBlockHeight + 1) { this.stateService.updateChainTip(response.block.height); - this.stateService.blocks$.next([response.block, response.txConfirmed || '']); + this.stateService.addBlock(response.block); + this.stateService.txConfirmed$.next([response.txConfirmed, response.block]); } else if (response.block.height > this.stateService.latestBlockHeight + 1) { reinitBlocks = true; }