Merge pull request #3937 from mempool/mononaut/replace-stale-blocks
Replace client-side stale blocks
This commit is contained in:
		
						commit
						cf13c43637
					
				| @ -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(); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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<void> { | ||||
|     if (!this.wss) { | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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() { | ||||
|  | ||||
| @ -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,13 +131,17 @@ 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(); | ||||
| 
 | ||||
|         for (const block of blocks) { | ||||
|           if (block.id === this.blockHash) { | ||||
|             this.block = block; | ||||
|             block.extras.minFee = this.getMinBlockFee(block); | ||||
| @ -142,6 +149,10 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|             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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
| 
 | ||||
|           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) { | ||||
| @ -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) { | ||||
|  | ||||
| @ -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; | ||||
|           }) | ||||
|         ) | ||||
|     ]) | ||||
|  | ||||
| @ -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
 | ||||
|       .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(); | ||||
|         } | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -60,15 +60,12 @@ 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); | ||||
|       .subscribe((blocks) => { | ||||
|         this.blocks = blocks.slice(0, 16); | ||||
|         if (this.blocks[this.blockIndex]) { | ||||
|           this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]); | ||||
|           this.cd.markForCheck(); | ||||
|         } | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     this.recommendedFees$ = this.stateService.recommendedFees$; | ||||
|  | ||||
| @ -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 = { | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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$(); | ||||
|           }) | ||||
|         ) | ||||
|  | ||||
| @ -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')) { | ||||
|  | ||||
| @ -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'; | ||||
| @ -49,10 +49,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 +131,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) => | ||||
| @ -391,9 +395,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, | ||||
| @ -593,13 +595,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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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') { | ||||
|  | ||||
| @ -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$ | ||||
|  | ||||
| @ -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.stateService.blocks$.subscribe((blocks) => { | ||||
|       for (const block of blocks) { | ||||
|         this.addBlockToCache(block); | ||||
|       } | ||||
|       this.clearBlocks(); | ||||
|     }); | ||||
|     this.stateService.chainTip$.subscribe((height) => { | ||||
| @ -56,9 +59,12 @@ export class CacheService { | ||||
|   } | ||||
| 
 | ||||
|   addBlockToCache(block: BlockExtended) { | ||||
|     if (!this.blockHashCache[block.id]) { | ||||
|       this.blockHashCache[block.id] = block; | ||||
|       this.blockCache[block.height] = block; | ||||
|       this.bumpBlockPriority(block.height); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async loadBlock(height) { | ||||
|     if (!this.blockCache[height] && !this.blockLoading[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 = {}; | ||||
|  | ||||
| @ -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<string>(1); | ||||
|   lightningChanged$ = new ReplaySubject<boolean>(1); | ||||
|   blocks$: ReplaySubject<[BlockExtended, string]>; | ||||
|   blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]); | ||||
|   blocks$: Observable<BlockExtended[]>; | ||||
|   transactions$ = new ReplaySubject<TransactionStripped>(6); | ||||
|   conversions$ = new ReplaySubject<any>(1); | ||||
|   bsqPrice$ = new ReplaySubject<number>(1); | ||||
| @ -102,6 +104,7 @@ export class StateService { | ||||
|   mempoolBlockTransactions$ = new Subject<TransactionStripped[]>(); | ||||
|   mempoolBlockDelta$ = new Subject<MempoolBlockDelta>(); | ||||
|   liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; | ||||
|   txConfirmed$ = new Subject<[string, BlockExtended]>(); | ||||
|   txReplaced$ = new Subject<ReplacedTransaction>(); | ||||
|   txRbfInfo$ = new Subject<RbfTree>(); | ||||
|   rbfLatest$ = new Subject<RbfTree[]>(); | ||||
| @ -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<TransactionStripped>(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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|       } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user