Merge pull request #2884 from mempool/mononaut/scrollable-blockchain
Infinitely scrolling blockchain
This commit is contained in:
		
						commit
						500f94227a
					
				| @ -402,7 +402,8 @@ class BitcoinRoutes { | ||||
|   private async getLegacyBlocks(req: Request, res: Response) { | ||||
|     try { | ||||
|       const returnBlocks: IEsploraApi.Block[] = []; | ||||
|       const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); | ||||
|       const tip = blocks.getCurrentBlockHeight(); | ||||
|       const fromHeight = Math.min(parseInt(req.params.height, 10) || tip, tip); | ||||
| 
 | ||||
|       // Check if block height exist in local cache to skip the hash lookup
 | ||||
|       const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); | ||||
|  | ||||
| @ -683,7 +683,12 @@ class Blocks { | ||||
|   } | ||||
| 
 | ||||
|   public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { | ||||
| 
 | ||||
|     let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; | ||||
|     if (currentHeight > this.currentBlockHeight) { | ||||
|       limit -= currentHeight - this.currentBlockHeight; | ||||
|       currentHeight = this.currentBlockHeight; | ||||
|     } | ||||
|     const returnBlocks: BlockExtended[] = []; | ||||
| 
 | ||||
|     if (currentHeight < 0) { | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './components/app/app.component'; | ||||
| import { ElectrsApiService } from './services/electrs-api.service'; | ||||
| import { StateService } from './services/state.service'; | ||||
| import { CacheService } from './services/cache.service'; | ||||
| import { EnterpriseService } from './services/enterprise.service'; | ||||
| import { WebsocketService } from './services/websocket.service'; | ||||
| import { AudioService } from './services/audio.service'; | ||||
| @ -23,6 +24,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; | ||||
| const providers = [ | ||||
|   ElectrsApiService, | ||||
|   StateService, | ||||
|   CacheService, | ||||
|   WebsocketService, | ||||
|   AudioService, | ||||
|   SeoService, | ||||
|  | ||||
| @ -42,6 +42,10 @@ export class AppComponent implements OnInit { | ||||
|     if (event.target instanceof HTMLInputElement) { | ||||
|       return; | ||||
|     } | ||||
|     // prevent arrow key horizontal scrolling
 | ||||
|     if(["ArrowLeft","ArrowRight"].indexOf(event.code) > -1) { | ||||
|       event.preventDefault(); | ||||
|     } | ||||
|     this.stateService.keyNavigation$.next(event); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -138,7 +138,6 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         this.page = 1; | ||||
|         this.error = undefined; | ||||
|         this.fees = undefined; | ||||
|         this.stateService.markBlock$.next({}); | ||||
|         this.auditDataMissing = false; | ||||
| 
 | ||||
|         if (history.state.data && history.state.data.blockHeight) { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate"> | ||||
|   <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" > | ||||
| <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [style.left]="static ? (offset || 0) + 'px' : null" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate"> | ||||
|   <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn"> | ||||
|     <ng-container *ngIf="block && !block.loading && !block.placeholder; else placeholderBlock"> | ||||
|       <div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)"> | ||||
|         <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" | ||||
|           class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a> | ||||
| @ -10,8 +11,11 @@ | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees"> | ||||
|             ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|           </div> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span"> | ||||
|           {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="block?.extras?.feeRange"> | ||||
|             {{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="!block?.extras?.feeRange"> | ||||
|               | ||||
|           </div> | ||||
|           <div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size"> | ||||
|             <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount> | ||||
| @ -29,8 +33,23 @@ | ||||
|             {{ block.extras.pool.name}}</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </ng-container> | ||||
|     <ng-template #placeholderBlock> | ||||
|       <ng-container *ngIf="block && block.placeholder; else loadingBlock"> | ||||
|         <div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block placeholder-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"> | ||||
|   | ||||
|         </div> | ||||
| <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | ||||
|       </ng-container> | ||||
|     </ng-template> | ||||
|     <ng-template #loadingBlock> | ||||
|       <ng-container *ngIf="block && block.loading"> | ||||
|         <div class="flashing"> | ||||
|           <div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"></div> | ||||
|         </div> | ||||
|       </ng-container> | ||||
|     </ng-template> | ||||
|   </div> | ||||
| <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingBlocksTemplate> | ||||
|  | ||||
| @ -25,6 +25,10 @@ | ||||
|   transition: background 2s, left 2s, transform 1s; | ||||
| } | ||||
| 
 | ||||
| .mined-block.placeholder-block { | ||||
|   background: none !important; | ||||
| } | ||||
| 
 | ||||
| .block-size { | ||||
|   font-size: 16px; | ||||
|   font-weight: bold; | ||||
| @ -96,6 +100,16 @@ | ||||
|   transform-origin: top; | ||||
| } | ||||
| 
 | ||||
| .bitcoin-block.placeholder-block::after { | ||||
|   content: none; | ||||
|   background: 0; | ||||
| } | ||||
| 
 | ||||
| .bitcoin-block.placeholder-block::before { | ||||
|   content: none; | ||||
|   background: 0; | ||||
| } | ||||
| 
 | ||||
| .black-background { | ||||
|   background-color: #11131f; | ||||
|   z-index: 100; | ||||
|  | ||||
| @ -1,10 +1,16 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Observable, Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { specialBlocks } from '../../app.constants'; | ||||
| import { BlockExtended } from '../../interfaces/node-api.interface'; | ||||
| import { Location } from '@angular/common'; | ||||
| import { config } from 'process'; | ||||
| import { CacheService } from 'src/app/services/cache.service'; | ||||
| 
 | ||||
| interface BlockchainBlock extends BlockExtended { | ||||
|   placeholder?: boolean; | ||||
|   loading?: boolean; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-blockchain-blocks', | ||||
| @ -12,13 +18,19 @@ import { config } from 'process'; | ||||
|   styleUrls: ['./blockchain-blocks.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
| export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   @Input() static: boolean = false; | ||||
|   @Input() offset: number = 0; | ||||
|   @Input() height: number = 0; | ||||
|   @Input() count: number = 8; | ||||
|    | ||||
|   specialBlocks = specialBlocks; | ||||
|   network = ''; | ||||
|   blocks: BlockExtended[] = []; | ||||
|   blocks: BlockchainBlock[] = []; | ||||
|   emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); | ||||
|   markHeight: number; | ||||
|   blocksSubscription: Subscription; | ||||
|   blockPageSubscription: Subscription; | ||||
|   networkSubscription: Subscription; | ||||
|   tabHiddenSubscription: Subscription; | ||||
|   markBlockSubscription: Subscription; | ||||
| @ -31,7 +43,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|   arrowVisible = false; | ||||
|   arrowLeftPx = 30; | ||||
|   blocksFilled = false; | ||||
|   transition = '1s'; | ||||
|   arrowTransition = '1s'; | ||||
|   showMiningInfo = false; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean; | ||||
| @ -47,6 +59,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     public cacheService: CacheService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     private location: Location, | ||||
|   ) { | ||||
| @ -75,6 +88,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|     this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; | ||||
|     this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||
|     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)) { | ||||
| @ -89,10 +103,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|           this.blocks.unshift(block); | ||||
|           this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||
| 
 | ||||
|         if (this.blocksFilled && !this.tabHidden && block.extras) { | ||||
|           block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2; | ||||
|         } | ||||
| 
 | ||||
|           if (txConfirmed) { | ||||
|             this.markHeight = block.height; | ||||
|             this.moveArrowToPosition(true, true); | ||||
| @ -101,18 +111,29 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|           } | ||||
| 
 | ||||
|           this.blockStyles = []; | ||||
|         this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); | ||||
|           if (this.blocksFilled) { | ||||
|             this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); | ||||
|             setTimeout(() => { | ||||
|               this.blockStyles = []; | ||||
|           this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); | ||||
|               this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|               this.cd.markForCheck(); | ||||
|             }, 50); | ||||
|           } else { | ||||
|             this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|           } | ||||
| 
 | ||||
|           if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { | ||||
|             this.blocksFilled = true; | ||||
|           } | ||||
|           this.cd.markForCheck(); | ||||
|         }); | ||||
|     } else { | ||||
|       this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => { | ||||
|         if (block.height <= this.height && block.height > this.height - this.count) { | ||||
|           this.onBlockLoaded(block); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.markBlockSubscription = this.stateService.markBlock$ | ||||
|       .subscribe((state) => { | ||||
| @ -123,10 +144,26 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         this.moveArrowToPosition(false); | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
| 
 | ||||
|       if (this.static) { | ||||
|         this.updateStaticBlocks(); | ||||
|       } | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (this.static) { | ||||
|       const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1); | ||||
|       this.updateStaticBlocks(animateSlide); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     if (this.blocksSubscription) { | ||||
|       this.blocksSubscription.unsubscribe(); | ||||
|     } | ||||
|     if (this.blockPageSubscription) { | ||||
|       this.blockPageSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.networkSubscription.unsubscribe(); | ||||
|     this.tabHiddenSubscription.unsubscribe(); | ||||
|     this.markBlockSubscription.unsubscribe(); | ||||
| @ -142,13 +179,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|     const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); | ||||
|     if (blockindex > -1) { | ||||
|       if (!animate) { | ||||
|         this.transition = 'inherit'; | ||||
|         this.arrowTransition = 'inherit'; | ||||
|       } | ||||
|       this.arrowVisible = true; | ||||
|       if (newBlockFromLeft) { | ||||
|         this.arrowLeftPx = blockindex * 155 + 30 - 205; | ||||
|         setTimeout(() => { | ||||
|           this.transition = '2s'; | ||||
|           this.arrowTransition = '2s'; | ||||
|           this.arrowLeftPx = blockindex * 155 + 30; | ||||
|           this.cd.markForCheck(); | ||||
|         }, 50); | ||||
| @ -156,46 +193,118 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         this.arrowLeftPx = blockindex * 155 + 30; | ||||
|         if (!animate) { | ||||
|           setTimeout(() => { | ||||
|             this.transition = '2s'; | ||||
|             this.arrowTransition = '2s'; | ||||
|             this.cd.markForCheck(); | ||||
|           }); | ||||
|           }, 50); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       this.arrowVisible = false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   trackByBlocksFn(index: number, item: BlockExtended) { | ||||
|   trackByBlocksFn(index: number, item: BlockchainBlock) { | ||||
|     return item.height; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForBlock(block: BlockExtended) { | ||||
|   updateStaticBlocks(animateSlide: boolean = false) { | ||||
|     // reset blocks
 | ||||
|     this.blocks = []; | ||||
|     this.blockStyles = []; | ||||
|     while (this.blocks.length < this.count) { | ||||
|       const height = this.height - this.blocks.length; | ||||
|       let block; | ||||
|       if (height >= 0) { | ||||
|         this.cacheService.loadBlock(height); | ||||
|         block = this.cacheService.getCachedBlock(height) || null; | ||||
|       } | ||||
|       this.blocks.push(block || { | ||||
|         placeholder: height < 0, | ||||
|         loading: height >= 0, | ||||
|         id: '', | ||||
|         height, | ||||
|         version: 0, | ||||
|         timestamp: 0, | ||||
|         bits: 0, | ||||
|         nonce: 0, | ||||
|         difficulty: 0, | ||||
|         merkle_root: '', | ||||
|         tx_count: 0, | ||||
|         size: 0, | ||||
|         weight: 0, | ||||
|         previousblockhash: '', | ||||
|       }); | ||||
|     } | ||||
|     this.blocks = this.blocks.slice(0, this.count); | ||||
|     this.blockStyles = []; | ||||
|     this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0))); | ||||
|     this.cd.markForCheck(); | ||||
|     if (animateSlide) { | ||||
|       // animate blocks slide right
 | ||||
|       setTimeout(() => { | ||||
|         this.blockStyles = []; | ||||
|         this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); | ||||
|         this.cd.markForCheck(); | ||||
|       }, 50); | ||||
|       this.moveArrowToPosition(true, true); | ||||
|     } else { | ||||
|       this.moveArrowToPosition(false, false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onBlockLoaded(block: BlockExtended) { | ||||
|     const blockIndex = this.height - block.height; | ||||
|     if (blockIndex >= 0 && blockIndex < this.blocks.length) { | ||||
|       this.blocks[blockIndex] = block; | ||||
|       this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex); | ||||
|     } | ||||
|     this.cd.markForCheck(); | ||||
|   } | ||||
| 
 | ||||
|   getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) { | ||||
|     if (!block || block.placeholder) { | ||||
|       return this.getStyleForPlaceholderBlock(index, animateEnterFrom); | ||||
|     } else if (block.loading) { | ||||
|       return this.getStyleForLoadingBlock(index, animateEnterFrom); | ||||
|     } | ||||
|     const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; | ||||
|     let addLeft = 0; | ||||
| 
 | ||||
|     if (block?.extras?.stage === 1) { | ||||
|       block.extras.stage = 2; | ||||
|       addLeft = -205; | ||||
|     if (animateEnterFrom) { | ||||
|       addLeft = animateEnterFrom || 0; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       left: addLeft + 155 * this.blocks.indexOf(block) + 'px', | ||||
|       left: addLeft + 155 * index + 'px', | ||||
|       background: `repeating-linear-gradient(
 | ||||
|         #2d3348, | ||||
|         #2d3348 ${greenBackgroundHeight}%, | ||||
|         ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, | ||||
|         ${this.gradientColors[this.network][1]} 100% | ||||
|       )`,
 | ||||
|       transition: animateEnterFrom ? 'background 2s, transform 1s' : null, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForEmptyBlock(block: BlockExtended) { | ||||
|     let addLeft = 0; | ||||
|   getStyleForLoadingBlock(index: number, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
| 
 | ||||
|     if (block?.extras?.stage === 1) { | ||||
|       block.extras.stage = 2; | ||||
|       addLeft = -205; | ||||
|     return { | ||||
|       left: addLeft + (155 * index) + 'px', | ||||
|       background: "#2d3348", | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
|     return { | ||||
|       left: addLeft + (155 * index) + 'px', | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getStyleForEmptyBlock(block: BlockExtended, animateEnterFrom: number = 0) { | ||||
|     const addLeft = animateEnterFrom || 0; | ||||
| 
 | ||||
|     return { | ||||
|       left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', | ||||
|       background: "#2d3348", | ||||
| @ -219,7 +328,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | ||||
|         weight: 0, | ||||
|         previousblockhash: '', | ||||
|         matchRate: 0, | ||||
|         stage: 0, | ||||
|       }); | ||||
|     } | ||||
|     return emptyBlocks; | ||||
|  | ||||
| @ -2,10 +2,14 @@ | ||||
|   <div class="position-container" [ngClass]="network ? network : ''"> | ||||
|     <span> | ||||
|       <div class="blocks-wrapper"> | ||||
|         <app-mempool-blocks></app-mempool-blocks> | ||||
|         <app-blockchain-blocks></app-blockchain-blocks> | ||||
|         <div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div> | ||||
|         <app-mempool-blocks [hidden]="pageIndex > 0"></app-mempool-blocks> | ||||
|         <app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks> | ||||
|         <ng-container *ngFor="let page of pages; trackBy: trackByPageFn"> | ||||
|           <app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage"></app-blockchain-blocks> | ||||
|         </ng-container> | ||||
|       </div> | ||||
|       <div id="divider"> | ||||
|       <div id="divider" [hidden]="pageIndex > 0"> | ||||
|         <button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button> | ||||
|       </div> | ||||
|     </span> | ||||
|  | ||||
| @ -72,6 +72,15 @@ | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .scroll-spacer { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 1px; | ||||
|   height: 1px; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .loading-block { | ||||
|   position: absolute; | ||||
|   text-align: center; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| 
 | ||||
| @ -9,6 +9,11 @@ import { StateService } from '../../services/state.service'; | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockchainComponent implements OnInit, OnDestroy { | ||||
|   @Input() pages: any[] = []; | ||||
|   @Input() pageIndex: number; | ||||
|   @Input() blocksPerPage: number = 8; | ||||
|   @Input() minScrollWidth: number = 0; | ||||
| 
 | ||||
|   network: string; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean = this.stateService.timeLtr.value; | ||||
| @ -29,6 +34,10 @@ export class BlockchainComponent implements OnInit, OnDestroy { | ||||
|     this.timeLtrSubscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   trackByPageFn(index: number, item: { index: number }) { | ||||
|     return item.index; | ||||
|   } | ||||
| 
 | ||||
|   toggleTimeDirection() { | ||||
|     this.ltrTransitionEnabled = true; | ||||
|     this.stateService.timeLtr.next(!this.timeLtr); | ||||
|  | ||||
| @ -11,8 +11,9 @@ | ||||
| <div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer | ||||
|   (mousedown)="onMouseDown($event)" | ||||
|   (dragstart)="onDragStart($event)" | ||||
|   (scroll)="onScroll($event)" | ||||
| > | ||||
| <app-blockchain></app-blockchain> | ||||
|   <app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain> | ||||
| </div> | ||||
| 
 | ||||
| <router-outlet></router-outlet> | ||||
|  | ||||
| @ -19,16 +19,51 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|   blockchainScrollLeftInit: number; | ||||
|   timeLtrSubscription: Subscription; | ||||
|   timeLtr: boolean = this.stateService.timeLtr.value; | ||||
|   chainTipSubscription: Subscription; | ||||
|   chainTip: number = -1; | ||||
|   markBlockSubscription: Subscription; | ||||
|   @ViewChild('blockchainContainer') blockchainContainer: ElementRef; | ||||
| 
 | ||||
|   isMobile: boolean = false; | ||||
|   blockWidth = 155; | ||||
|   blocksPerPage: number = 1; | ||||
|   pageWidth: number; | ||||
|   firstPageWidth: number; | ||||
|   minScrollWidth: number; | ||||
|   pageIndex: number = 0; | ||||
|   pages: any[] = []; | ||||
|   pendingMark: number | void = null; | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.firstPageWidth = 40 + (this.blockWidth * this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||
|     this.onResize(); | ||||
|     this.updatePages(); | ||||
|     this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { | ||||
|       this.timeLtr = !!ltr; | ||||
|     }); | ||||
|     this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => { | ||||
|       this.chainTip = height; | ||||
|       this.updatePages(); | ||||
|       if (this.pendingMark != null) { | ||||
|         this.scrollToBlock(this.pendingMark); | ||||
|         this.pendingMark = null; | ||||
|       } | ||||
|     }); | ||||
|     this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => { | ||||
|       if (mark?.blockHeight != null) { | ||||
|         if (this.chainTip >=0) { | ||||
|           if (!this.blockInViewport(mark.blockHeight)) { | ||||
|             this.scrollToBlock(mark.blockHeight); | ||||
|           } | ||||
|         } else { | ||||
|           this.pendingMark = mark.blockHeight; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     this.stateService.blocks$ | ||||
|       .subscribe((blocks: any) => { | ||||
|         if (this.stateService.network !== '') { | ||||
| @ -55,6 +90,34 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|     let firstVisibleBlock; | ||||
|     let offset; | ||||
|     if (this.blockchainContainer?.nativeElement != null) { | ||||
|       this.pages.forEach(page => { | ||||
|         const left = page.offset - this.getConvertedScrollOffset(); | ||||
|         const right = left + this.pageWidth; | ||||
|         if (left <= 0 && right > 0) { | ||||
|           const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth)); | ||||
|           firstVisibleBlock = page.height - blockIndex; | ||||
|           offset = left + (blockIndex * this.blockWidth); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth); | ||||
|     this.pageWidth = this.blocksPerPage * this.blockWidth; | ||||
|     this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2); | ||||
| 
 | ||||
|     if (firstVisibleBlock != null) { | ||||
|       this.scrollToBlock(firstVisibleBlock, offset); | ||||
|     } else { | ||||
|       this.updatePages(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onMouseDown(event: MouseEvent) { | ||||
|     this.mouseDragStartX = event.clientX; | ||||
|     this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; | ||||
| @ -70,7 +133,7 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|     if (this.mouseDragStartX != null) { | ||||
|       this.stateService.setBlockScrollingInProgress(true); | ||||
|       this.blockchainContainer.nativeElement.scrollLeft = | ||||
|         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX | ||||
|         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX; | ||||
|     } | ||||
|   } | ||||
|   @HostListener('document:mouseup', []) | ||||
| @ -79,7 +142,149 @@ export class StartComponent implements OnInit, OnDestroy { | ||||
|     this.stateService.setBlockScrollingInProgress(false); | ||||
|   } | ||||
| 
 | ||||
|   onScroll(e) { | ||||
|     const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1]; | ||||
|     // compensate for css transform
 | ||||
|     const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); | ||||
|     const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation; | ||||
|     const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation; | ||||
|     const scrollLeft = this.getConvertedScrollOffset(); | ||||
|     if (scrollLeft > backThreshold) { | ||||
|       if (this.shiftPagesBack()) { | ||||
|         this.addConvertedScrollOffset(-this.pageWidth); | ||||
|         this.blockchainScrollLeftInit -= this.pageWidth; | ||||
|       } | ||||
|     } else if (scrollLeft < forwardThreshold) { | ||||
|       if (this.shiftPagesForward()) { | ||||
|         this.addConvertedScrollOffset(this.pageWidth); | ||||
|         this.blockchainScrollLeftInit += this.pageWidth; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   scrollToBlock(height, blockOffset = 0) { | ||||
|     if (!this.blockchainContainer?.nativeElement) { | ||||
|       setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50); | ||||
|       return; | ||||
|     } | ||||
|     const targetHeight = this.isMobile ? height - 1 : height; | ||||
|     const viewingPageIndex = this.getPageIndexOf(targetHeight); | ||||
|     const pages = []; | ||||
|     this.pageIndex = Math.max(viewingPageIndex - 1, 0); | ||||
|     let viewingPage = this.getPageAt(viewingPageIndex); | ||||
|     const isLastPage = viewingPage.height < this.blocksPerPage; | ||||
|     if (isLastPage) { | ||||
|       this.pageIndex = Math.max(viewingPageIndex - 2, 0); | ||||
|       viewingPage = this.getPageAt(viewingPageIndex); | ||||
|     } | ||||
|     const left = viewingPage.offset - this.getConvertedScrollOffset(); | ||||
|     const blockIndex = viewingPage.height - targetHeight; | ||||
|     const targetOffset = (this.blockWidth * blockIndex) + left; | ||||
|     let deltaOffset = targetOffset - blockOffset; | ||||
| 
 | ||||
|     if (isLastPage) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex - 2)); | ||||
|     } | ||||
|     if (viewingPageIndex > 1) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex - 1)); | ||||
|     } | ||||
|     if (viewingPageIndex > 0) { | ||||
|       pages.push(viewingPage); | ||||
|     } | ||||
|     if (!isLastPage) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex + 1)); | ||||
|     } | ||||
|     if (viewingPageIndex === 0) { | ||||
|       pages.push(this.getPageAt(viewingPageIndex + 2)); | ||||
|     } | ||||
| 
 | ||||
|     this.pages = pages; | ||||
|     this.addConvertedScrollOffset(deltaOffset); | ||||
|   } | ||||
| 
 | ||||
|   updatePages() { | ||||
|     const pages = []; | ||||
|     if (this.pageIndex > 0) { | ||||
|       pages.push(this.getPageAt(this.pageIndex)); | ||||
|     } | ||||
|     pages.push(this.getPageAt(this.pageIndex + 1)); | ||||
|     pages.push(this.getPageAt(this.pageIndex + 2)); | ||||
|     this.pages = pages; | ||||
|   } | ||||
| 
 | ||||
|   shiftPagesBack(): boolean { | ||||
|     const nextPage = this.getPageAt(this.pageIndex + 3); | ||||
|     if (nextPage.height >= 0) { | ||||
|       this.pageIndex++; | ||||
|       this.pages.forEach(page => page.offset -= this.pageWidth); | ||||
|       if (this.pageIndex !== 1) { | ||||
|         this.pages.shift(); | ||||
|       } | ||||
|       this.pages.push(this.getPageAt(this.pageIndex + 2)); | ||||
|      return true; | ||||
|     } else { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   shiftPagesForward(): boolean { | ||||
|     if (this.pageIndex > 0) { | ||||
|       this.pageIndex--; | ||||
|       this.pages.forEach(page => page.offset += this.pageWidth); | ||||
|       this.pages.pop(); | ||||
|       if (this.pageIndex) { | ||||
|         this.pages.unshift(this.getPageAt(this.pageIndex)); | ||||
|       } | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   getPageAt(index: number) { | ||||
|     const height = this.chainTip - 8 - ((index - 1) * this.blocksPerPage) | ||||
|     return { | ||||
|       offset: this.firstPageWidth + (this.pageWidth * (index - 1 - this.pageIndex)), | ||||
|       height: height, | ||||
|       depth: this.chainTip - height, | ||||
|       index: index, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   getPageIndexOf(height: number): number { | ||||
|     const delta = this.chainTip - 8 - height; | ||||
|     return Math.max(0, Math.floor(delta / this.blocksPerPage) + 1); | ||||
|   } | ||||
| 
 | ||||
|   blockInViewport(height: number): boolean { | ||||
|     const firstHeight = this.pages[0].height; | ||||
|     const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); | ||||
|     const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation; | ||||
|     const xPos = firstX + ((firstHeight - height) * 155); | ||||
|     return xPos > -55 && xPos < (window.innerWidth - 100); | ||||
|   } | ||||
| 
 | ||||
|   getConvertedScrollOffset(): number { | ||||
|     if (this.timeLtr) { | ||||
|       return -this.blockchainContainer?.nativeElement?.scrollLeft || 0; | ||||
|     } else { | ||||
|       return this.blockchainContainer?.nativeElement?.scrollLeft || 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   addConvertedScrollOffset(offset: number): void { | ||||
|     if (!this.blockchainContainer?.nativeElement) { | ||||
|       return; | ||||
|     } | ||||
|     if (this.timeLtr) { | ||||
|       this.blockchainContainer.nativeElement.scrollLeft -= offset; | ||||
|     } else { | ||||
|       this.blockchainContainer.nativeElement.scrollLeft += offset; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.timeLtrSubscription.unsubscribe(); | ||||
|     this.chainTipSubscription.unsubscribe(); | ||||
|     this.markBlockSubscription.unsubscribe(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { | ||||
| import { Transaction, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { OpenGraphService } from '../../services/opengraph.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| @ -45,6 +46,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | ||||
|     private route: ActivatedRoute, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private apiService: ApiService, | ||||
|     private seoService: SeoService, | ||||
|     private openGraphService: OpenGraphService, | ||||
| @ -97,7 +99,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | ||||
|         }), | ||||
|         switchMap(() => { | ||||
|           let transactionObservable$: Observable<Transaction>; | ||||
|           const cached = this.stateService.getTxFromCache(this.txId); | ||||
|           const cached = this.cacheService.getTxFromCache(this.txId); | ||||
|           if (cached && cached.fee !== -1) { | ||||
|             transactionObservable$ = of(cached); | ||||
|           } else { | ||||
|  | ||||
| @ -13,6 +13,7 @@ import { | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { AudioService } from '../../services/audio.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| @ -74,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     private relativeUrlPipe: RelativeUrlPipe, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private audioService: AudioService, | ||||
|     private apiService: ApiService, | ||||
| @ -197,7 +199,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         }), | ||||
|         switchMap(() => { | ||||
|           let transactionObservable$: Observable<Transaction>; | ||||
|           const cached = this.stateService.getTxFromCache(this.txId); | ||||
|           const cached = this.cacheService.getTxFromCache(this.txId); | ||||
|           if (cached && cached.fee !== -1) { | ||||
|             transactionObservable$ = of(cached); | ||||
|           } else { | ||||
| @ -296,7 +298,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         this.waitingForTransaction = false; | ||||
|       } | ||||
|       this.rbfTransaction = rbfTransaction; | ||||
|       this.stateService.setTxCache([this.rbfTransaction]); | ||||
|       this.cacheService.setTxCache([this.rbfTransaction]); | ||||
|     }); | ||||
| 
 | ||||
|     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; | ||||
| import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| @ -44,6 +45,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private apiService: ApiService, | ||||
|     private assetsService: AssetsService, | ||||
| @ -123,7 +125,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | ||||
|       } | ||||
| 
 | ||||
|       this.transactionsLength = this.transactions.length; | ||||
|       this.stateService.setTxCache(this.transactions); | ||||
|       this.cacheService.setTxCache(this.transactions); | ||||
| 
 | ||||
|       this.transactions.forEach((tx) => { | ||||
|         tx['@voutLimit'] = true; | ||||
|  | ||||
| @ -121,8 +121,6 @@ export interface BlockExtension { | ||||
|     name: string; | ||||
|     slug: string; | ||||
|   } | ||||
| 
 | ||||
|   stage?: number; // Frontend only
 | ||||
| } | ||||
| 
 | ||||
| export interface BlockExtended extends Block { | ||||
|  | ||||
							
								
								
									
										105
									
								
								frontend/src/app/services/cache.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								frontend/src/app/services/cache.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { firstValueFrom, Subject, Subscription} from 'rxjs'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { BlockExtended } from '../interfaces/node-api.interface'; | ||||
| import { StateService } from './state.service'; | ||||
| import { ApiService } from './api.service'; | ||||
| 
 | ||||
| const BLOCK_CACHE_SIZE = 500; | ||||
| const KEEP_RECENT_BLOCKS = 50; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class CacheService { | ||||
|   loadedBlocks$ = new Subject<BlockExtended>(); | ||||
|   tip: number = 0; | ||||
| 
 | ||||
|   txCache: { [txid: string]: Transaction } = {}; | ||||
| 
 | ||||
|   blockCache: { [height: number]: BlockExtended } = {}; | ||||
|   blockLoading: { [height: number]: boolean } = {}; | ||||
|   copiesInBlockQueue: { [height: number]: number } = {}; | ||||
|   blockPriorities: number[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|     private apiService: ApiService, | ||||
|   ) { | ||||
|     this.stateService.blocks$.subscribe(([block]) => { | ||||
|       this.addBlockToCache(block); | ||||
|       this.clearBlocks(); | ||||
|     }); | ||||
|     this.stateService.chainTip$.subscribe((height) => { | ||||
|       this.tip = height; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   setTxCache(transactions) { | ||||
|     this.txCache = {}; | ||||
|     transactions.forEach(tx => { | ||||
|       this.txCache[tx.txid] = tx; | ||||
|     }); | ||||
|   } | ||||
|   | ||||
|   getTxFromCache(txid) { | ||||
|     if (this.txCache && this.txCache[txid]) { | ||||
|       return this.txCache[txid]; | ||||
|     } else { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   addBlockToCache(block: BlockExtended) { | ||||
|     this.blockCache[block.height] = block; | ||||
|     this.bumpBlockPriority(block.height); | ||||
|   } | ||||
| 
 | ||||
|   async loadBlock(height) { | ||||
|     if (!this.blockCache[height] && !this.blockLoading[height]) { | ||||
|       const chunkSize = 10; | ||||
|       const maxHeight = Math.ceil(height / chunkSize) * chunkSize; | ||||
|       for (let i = 0; i < chunkSize; i++) { | ||||
|         this.blockLoading[maxHeight - i] = true; | ||||
|       } | ||||
|       const result = await firstValueFrom(this.apiService.getBlocks$(maxHeight)); | ||||
|       for (let i = 0; i < chunkSize; i++) { | ||||
|         delete this.blockLoading[maxHeight - i]; | ||||
|       } | ||||
|       if (result && result.length) { | ||||
|         result.forEach(block => { | ||||
|           this.addBlockToCache(block); | ||||
|           this.loadedBlocks$.next(block); | ||||
|         }); | ||||
|       } | ||||
|       this.clearBlocks(); | ||||
|     } else { | ||||
|       this.bumpBlockPriority(height); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // increase the priority of a block, to delay removal
 | ||||
|   bumpBlockPriority(height) { | ||||
|     this.blockPriorities.push(height); | ||||
|     this.copiesInBlockQueue[height] = (this.copiesInBlockQueue[height] || 0) + 1; | ||||
|   } | ||||
| 
 | ||||
|   // remove lowest priority blocks from the cache
 | ||||
|   clearBlocks() { | ||||
|     while (Object.keys(this.blockCache).length > (BLOCK_CACHE_SIZE + KEEP_RECENT_BLOCKS) && this.blockPriorities.length > KEEP_RECENT_BLOCKS) { | ||||
|       const height = this.blockPriorities.shift(); | ||||
|       if (this.copiesInBlockQueue[height] > 1) { | ||||
|         this.copiesInBlockQueue[height]--; | ||||
|       } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) { | ||||
|         this.bumpBlockPriority(height); | ||||
|       } else { | ||||
|         delete this.blockCache[height]; | ||||
|         delete this.copiesInBlockQueue[height]; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getCachedBlock(height) { | ||||
|     return this.blockCache[height]; | ||||
|   } | ||||
| } | ||||
| @ -104,6 +104,7 @@ export class StateService { | ||||
|   backendInfo$ = new ReplaySubject<IBackendInfo>(1); | ||||
|   loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1); | ||||
|   recommendedFees$ = new ReplaySubject<Recommendedfees>(1); | ||||
|   chainTip$ = new ReplaySubject<number>(-1); | ||||
| 
 | ||||
|   live2Chart$ = new Subject<OptimizedMempoolStats>(); | ||||
| 
 | ||||
| @ -111,15 +112,13 @@ export class StateService { | ||||
|   connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); | ||||
|   isTabHidden$: Observable<boolean>; | ||||
| 
 | ||||
|   markBlock$ = new ReplaySubject<MarkBlockState>(); | ||||
|   markBlock$ = new BehaviorSubject<MarkBlockState>({}); | ||||
|   keyNavigation$ = new Subject<KeyboardEvent>(); | ||||
| 
 | ||||
|   blockScrolling$: Subject<boolean> = new Subject<boolean>(); | ||||
|   timeLtr: BehaviorSubject<boolean>; | ||||
|   hideFlow: BehaviorSubject<boolean>; | ||||
| 
 | ||||
|   txCache: { [txid: string]: Transaction } = {}; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(PLATFORM_ID) private platformId: any, | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
| @ -274,18 +273,15 @@ export class StateService { | ||||
|     return this.network === 'liquid' || this.network === 'liquidtestnet'; | ||||
|   } | ||||
| 
 | ||||
|   setTxCache(transactions) { | ||||
|     this.txCache = {}; | ||||
|     transactions.forEach(tx => { | ||||
|       this.txCache[tx.txid] = tx; | ||||
|     }); | ||||
|   resetChainTip() { | ||||
|     this.latestBlockHeight = -1; | ||||
|     this.chainTip$.next(-1); | ||||
|   } | ||||
| 
 | ||||
|   getTxFromCache(txid) { | ||||
|     if (this.txCache && this.txCache[txid]) { | ||||
|       return this.txCache[txid]; | ||||
|     } else { | ||||
|       return null; | ||||
|   updateChainTip(height) { | ||||
|     if (height > this.latestBlockHeight) { | ||||
|       this.latestBlockHeight = height; | ||||
|       this.chainTip$.next(height); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -70,7 +70,7 @@ export class WebsocketService { | ||||
|         clearTimeout(this.onlineCheckTimeout); | ||||
|         clearTimeout(this.onlineCheckTimeoutTwo); | ||||
| 
 | ||||
|         this.stateService.latestBlockHeight = -1; | ||||
|         this.stateService.resetChainTip(); | ||||
| 
 | ||||
|         this.websocketSubject.complete(); | ||||
|         this.subscription.unsubscribe(); | ||||
| @ -224,12 +224,14 @@ export class WebsocketService { | ||||
|   handleResponse(response: WebsocketResponse) { | ||||
|     if (response.blocks && response.blocks.length) { | ||||
|       const blocks = response.blocks; | ||||
|       let maxHeight = 0; | ||||
|       blocks.forEach((block: BlockExtended) => { | ||||
|         if (block.height > this.stateService.latestBlockHeight) { | ||||
|           this.stateService.latestBlockHeight = block.height; | ||||
|           maxHeight = Math.max(maxHeight, block.height); | ||||
|           this.stateService.blocks$.next([block, false]); | ||||
|         } | ||||
|       }); | ||||
|       this.stateService.updateChainTip(maxHeight); | ||||
|     } | ||||
| 
 | ||||
|     if (response.tx) { | ||||
| @ -238,7 +240,7 @@ export class WebsocketService { | ||||
| 
 | ||||
|     if (response.block) { | ||||
|       if (response.block.height > this.stateService.latestBlockHeight) { | ||||
|         this.stateService.latestBlockHeight = response.block.height; | ||||
|         this.stateService.updateChainTip(response.block.height); | ||||
|         this.stateService.blocks$.next([response.block, !!response.txConfirmed]); | ||||
|       } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user