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) { |   private async getLegacyBlocks(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       const returnBlocks: IEsploraApi.Block[] = []; |       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
 |       // Check if block height exist in local cache to skip the hash lookup
 | ||||||
|       const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); |       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[]> { |   public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { | ||||||
|  | 
 | ||||||
|     let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; |     let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; | ||||||
|  |     if (currentHeight > this.currentBlockHeight) { | ||||||
|  |       limit -= currentHeight - this.currentBlockHeight; | ||||||
|  |       currentHeight = this.currentBlockHeight; | ||||||
|  |     } | ||||||
|     const returnBlocks: BlockExtended[] = []; |     const returnBlocks: BlockExtended[] = []; | ||||||
| 
 | 
 | ||||||
|     if (currentHeight < 0) { |     if (currentHeight < 0) { | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module'; | |||||||
| import { AppComponent } from './components/app/app.component'; | import { AppComponent } from './components/app/app.component'; | ||||||
| import { ElectrsApiService } from './services/electrs-api.service'; | import { ElectrsApiService } from './services/electrs-api.service'; | ||||||
| import { StateService } from './services/state.service'; | import { StateService } from './services/state.service'; | ||||||
|  | import { CacheService } from './services/cache.service'; | ||||||
| import { EnterpriseService } from './services/enterprise.service'; | import { EnterpriseService } from './services/enterprise.service'; | ||||||
| import { WebsocketService } from './services/websocket.service'; | import { WebsocketService } from './services/websocket.service'; | ||||||
| import { AudioService } from './services/audio.service'; | import { AudioService } from './services/audio.service'; | ||||||
| @ -23,6 +24,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; | |||||||
| const providers = [ | const providers = [ | ||||||
|   ElectrsApiService, |   ElectrsApiService, | ||||||
|   StateService, |   StateService, | ||||||
|  |   CacheService, | ||||||
|   WebsocketService, |   WebsocketService, | ||||||
|   AudioService, |   AudioService, | ||||||
|   SeoService, |   SeoService, | ||||||
|  | |||||||
| @ -42,6 +42,10 @@ export class AppComponent implements OnInit { | |||||||
|     if (event.target instanceof HTMLInputElement) { |     if (event.target instanceof HTMLInputElement) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     // prevent arrow key horizontal scrolling
 | ||||||
|  |     if(["ArrowLeft","ArrowRight"].indexOf(event.code) > -1) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |     } | ||||||
|     this.stateService.keyNavigation$.next(event); |     this.stateService.keyNavigation$.next(event); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -138,7 +138,6 @@ export class BlockComponent implements OnInit, OnDestroy { | |||||||
|         this.page = 1; |         this.page = 1; | ||||||
|         this.error = undefined; |         this.error = undefined; | ||||||
|         this.fees = undefined; |         this.fees = undefined; | ||||||
|         this.stateService.markBlock$.next({}); |  | ||||||
|         this.auditDataMissing = false; |         this.auditDataMissing = false; | ||||||
| 
 | 
 | ||||||
|         if (history.state.data && history.state.data.blockHeight) { |         if (history.state.data && history.state.data.blockHeight) { | ||||||
|  | |||||||
| @ -1,36 +1,55 @@ | |||||||
| <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate"> | <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" > |   <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn"> | ||||||
|     <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)"> |     <ng-container *ngIf="block && !block.loading && !block.placeholder; else placeholderBlock"> | ||||||
|       <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" |       <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)"> | ||||||
|         class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a> |         <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" | ||||||
|       <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> |           class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a> | ||||||
|         <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> |         <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> | ||||||
|  |           <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> | ||||||
|  |         </div> | ||||||
|  |         <div class="block-body"> | ||||||
|  |           <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" *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> | ||||||
|  |           </div> | ||||||
|  |           <div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div> | ||||||
|  |           <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count"> | ||||||
|  |             <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> | ||||||
|  |             <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> | ||||||
|  |             <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> | ||||||
|  |           </div> | ||||||
|  |           <div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> | ||||||
|  |         </div> | ||||||
|  |         <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> | ||||||
|  |           <a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> | ||||||
|  |             {{ block.extras.pool.name}}</a> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div class="block-body"> |     </ng-container> | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees"> |     <ng-template #placeholderBlock> | ||||||
|           ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> |       <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> | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span"> |       </ng-container> | ||||||
|           {{ 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> |     </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> |         </div> | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size"> |       </ng-container> | ||||||
|           <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount> |     </ng-template> | ||||||
|         </div> |  | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div> |  | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count"> |  | ||||||
|           <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> |  | ||||||
|           <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> |  | ||||||
|           <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> |  | ||||||
|         </div> |  | ||||||
|         <div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> |  | ||||||
|       </div> |  | ||||||
|       <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> |  | ||||||
|         <a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> |  | ||||||
|           {{ block.extras.pool.name}}</a> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |   </div> | ||||||
| <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <ng-template #loadingBlocksTemplate> | <ng-template #loadingBlocksTemplate> | ||||||
|  | |||||||
| @ -25,6 +25,10 @@ | |||||||
|   transition: background 2s, left 2s, transform 1s; |   transition: background 2s, left 2s, transform 1s; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .mined-block.placeholder-block { | ||||||
|  |   background: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .block-size { | .block-size { | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
|   font-weight: bold; |   font-weight: bold; | ||||||
| @ -96,6 +100,16 @@ | |||||||
|   transform-origin: top; |   transform-origin: top; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .bitcoin-block.placeholder-block::after { | ||||||
|  |   content: none; | ||||||
|  |   background: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .bitcoin-block.placeholder-block::before { | ||||||
|  |   content: none; | ||||||
|  |   background: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .black-background { | .black-background { | ||||||
|   background-color: #11131f; |   background-color: #11131f; | ||||||
|   z-index: 100; |   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 { Observable, Subscription } from 'rxjs'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { specialBlocks } from '../../app.constants'; | import { specialBlocks } from '../../app.constants'; | ||||||
| import { BlockExtended } from '../../interfaces/node-api.interface'; | import { BlockExtended } from '../../interfaces/node-api.interface'; | ||||||
| import { Location } from '@angular/common'; | import { Location } from '@angular/common'; | ||||||
| import { config } from 'process'; | import { config } from 'process'; | ||||||
|  | import { CacheService } from 'src/app/services/cache.service'; | ||||||
|  | 
 | ||||||
|  | interface BlockchainBlock extends BlockExtended { | ||||||
|  |   placeholder?: boolean; | ||||||
|  |   loading?: boolean; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-blockchain-blocks', |   selector: 'app-blockchain-blocks', | ||||||
| @ -12,13 +18,19 @@ import { config } from 'process'; | |||||||
|   styleUrls: ['./blockchain-blocks.component.scss'], |   styleUrls: ['./blockchain-blocks.component.scss'], | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   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; |   specialBlocks = specialBlocks; | ||||||
|   network = ''; |   network = ''; | ||||||
|   blocks: BlockExtended[] = []; |   blocks: BlockchainBlock[] = []; | ||||||
|   emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); |   emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); | ||||||
|   markHeight: number; |   markHeight: number; | ||||||
|   blocksSubscription: Subscription; |   blocksSubscription: Subscription; | ||||||
|  |   blockPageSubscription: Subscription; | ||||||
|   networkSubscription: Subscription; |   networkSubscription: Subscription; | ||||||
|   tabHiddenSubscription: Subscription; |   tabHiddenSubscription: Subscription; | ||||||
|   markBlockSubscription: Subscription; |   markBlockSubscription: Subscription; | ||||||
| @ -31,7 +43,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|   arrowVisible = false; |   arrowVisible = false; | ||||||
|   arrowLeftPx = 30; |   arrowLeftPx = 30; | ||||||
|   blocksFilled = false; |   blocksFilled = false; | ||||||
|   transition = '1s'; |   arrowTransition = '1s'; | ||||||
|   showMiningInfo = false; |   showMiningInfo = false; | ||||||
|   timeLtrSubscription: Subscription; |   timeLtrSubscription: Subscription; | ||||||
|   timeLtr: boolean; |   timeLtr: boolean; | ||||||
| @ -47,6 +59,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|  |     public cacheService: CacheService, | ||||||
|     private cd: ChangeDetectorRef, |     private cd: ChangeDetectorRef, | ||||||
|     private location: Location, |     private location: Location, | ||||||
|   ) { |   ) { | ||||||
| @ -75,44 +88,52 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|     this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; |     this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; | ||||||
|     this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); |     this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); | ||||||
|     this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); |     this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); | ||||||
|     this.blocksSubscription = this.stateService.blocks$ |     if (!this.static) { | ||||||
|       .subscribe(([block, txConfirmed]) => { |       this.blocksSubscription = this.stateService.blocks$ | ||||||
|         if (this.blocks.some((b) => b.height === block.height)) { |         .subscribe(([block, txConfirmed]) => { | ||||||
|           return; |           if (this.blocks.some((b) => b.height === block.height)) { | ||||||
|         } |             return; | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|         if (this.blocks.length && block.height !== this.blocks[0].height + 1) { |           if (this.blocks.length && block.height !== this.blocks[0].height + 1) { | ||||||
|           this.blocks = []; |             this.blocks = []; | ||||||
|           this.blocksFilled = false; |             this.blocksFilled = false; | ||||||
|         } |           } | ||||||
| 
 | 
 | ||||||
|         this.blocks.unshift(block); |           this.blocks.unshift(block); | ||||||
|         this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); |           this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||||
| 
 | 
 | ||||||
|         if (this.blocksFilled && !this.tabHidden && block.extras) { |           if (txConfirmed) { | ||||||
|           block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2; |             this.markHeight = block.height; | ||||||
|         } |             this.moveArrowToPosition(true, true); | ||||||
|  |           } else { | ||||||
|  |             this.moveArrowToPosition(true, false); | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|         if (txConfirmed) { |  | ||||||
|           this.markHeight = block.height; |  | ||||||
|           this.moveArrowToPosition(true, true); |  | ||||||
|         } else { |  | ||||||
|           this.moveArrowToPosition(true, false); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.blockStyles = []; |  | ||||||
|         this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); |  | ||||||
|         setTimeout(() => { |  | ||||||
|           this.blockStyles = []; |           this.blockStyles = []; | ||||||
|           this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); |           if (this.blocksFilled) { | ||||||
|           this.cd.markForCheck(); |             this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); | ||||||
|         }, 50); |             setTimeout(() => { | ||||||
|  |               this.blockStyles = []; | ||||||
|  |               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) { |           if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { | ||||||
|           this.blocksFilled = true; |             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.cd.markForCheck(); |  | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     this.markBlockSubscription = this.stateService.markBlock$ |     this.markBlockSubscription = this.stateService.markBlock$ | ||||||
|       .subscribe((state) => { |       .subscribe((state) => { | ||||||
| @ -123,10 +144,26 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|         this.moveArrowToPosition(false); |         this.moveArrowToPosition(false); | ||||||
|         this.cd.markForCheck(); |         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() { |   ngOnDestroy() { | ||||||
|     this.blocksSubscription.unsubscribe(); |     if (this.blocksSubscription) { | ||||||
|  |       this.blocksSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     if (this.blockPageSubscription) { | ||||||
|  |       this.blockPageSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|     this.networkSubscription.unsubscribe(); |     this.networkSubscription.unsubscribe(); | ||||||
|     this.tabHiddenSubscription.unsubscribe(); |     this.tabHiddenSubscription.unsubscribe(); | ||||||
|     this.markBlockSubscription.unsubscribe(); |     this.markBlockSubscription.unsubscribe(); | ||||||
| @ -142,13 +179,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|     const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); |     const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); | ||||||
|     if (blockindex > -1) { |     if (blockindex > -1) { | ||||||
|       if (!animate) { |       if (!animate) { | ||||||
|         this.transition = 'inherit'; |         this.arrowTransition = 'inherit'; | ||||||
|       } |       } | ||||||
|       this.arrowVisible = true; |       this.arrowVisible = true; | ||||||
|       if (newBlockFromLeft) { |       if (newBlockFromLeft) { | ||||||
|         this.arrowLeftPx = blockindex * 155 + 30 - 205; |         this.arrowLeftPx = blockindex * 155 + 30 - 205; | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|           this.transition = '2s'; |           this.arrowTransition = '2s'; | ||||||
|           this.arrowLeftPx = blockindex * 155 + 30; |           this.arrowLeftPx = blockindex * 155 + 30; | ||||||
|           this.cd.markForCheck(); |           this.cd.markForCheck(); | ||||||
|         }, 50); |         }, 50); | ||||||
| @ -156,45 +193,117 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|         this.arrowLeftPx = blockindex * 155 + 30; |         this.arrowLeftPx = blockindex * 155 + 30; | ||||||
|         if (!animate) { |         if (!animate) { | ||||||
|           setTimeout(() => { |           setTimeout(() => { | ||||||
|             this.transition = '2s'; |             this.arrowTransition = '2s'; | ||||||
|             this.cd.markForCheck(); |             this.cd.markForCheck(); | ||||||
|           }); |           }, 50); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |     } else { | ||||||
|  |       this.arrowVisible = false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   trackByBlocksFn(index: number, item: BlockExtended) { |   trackByBlocksFn(index: number, item: BlockchainBlock) { | ||||||
|     return item.height; |     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; |     const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; | ||||||
|     let addLeft = 0; |     let addLeft = 0; | ||||||
| 
 | 
 | ||||||
|     if (block?.extras?.stage === 1) { |     if (animateEnterFrom) { | ||||||
|       block.extras.stage = 2; |       addLeft = animateEnterFrom || 0; | ||||||
|       addLeft = -205; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       left: addLeft + 155 * this.blocks.indexOf(block) + 'px', |       left: addLeft + 155 * index + 'px', | ||||||
|       background: `repeating-linear-gradient(
 |       background: `repeating-linear-gradient(
 | ||||||
|         #2d3348, |         #2d3348, | ||||||
|         #2d3348 ${greenBackgroundHeight}%, |         #2d3348 ${greenBackgroundHeight}%, | ||||||
|         ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, |         ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, | ||||||
|         ${this.gradientColors[this.network][1]} 100% |         ${this.gradientColors[this.network][1]} 100% | ||||||
|       )`,
 |       )`,
 | ||||||
|  |       transition: animateEnterFrom ? 'background 2s, transform 1s' : null, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getStyleForEmptyBlock(block: BlockExtended) { |   getStyleForLoadingBlock(index: number, animateEnterFrom: number = 0) { | ||||||
|     let addLeft = 0; |     const addLeft = animateEnterFrom || 0; | ||||||
| 
 | 
 | ||||||
|     if (block?.extras?.stage === 1) { |     return { | ||||||
|       block.extras.stage = 2; |       left: addLeft + (155 * index) + 'px', | ||||||
|       addLeft = -205; |       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 { |     return { | ||||||
|       left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', |       left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', | ||||||
| @ -219,7 +328,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { | |||||||
|         weight: 0, |         weight: 0, | ||||||
|         previousblockhash: '', |         previousblockhash: '', | ||||||
|         matchRate: 0, |         matchRate: 0, | ||||||
|         stage: 0, |  | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     return emptyBlocks; |     return emptyBlocks; | ||||||
|  | |||||||
| @ -2,10 +2,14 @@ | |||||||
|   <div class="position-container" [ngClass]="network ? network : ''"> |   <div class="position-container" [ngClass]="network ? network : ''"> | ||||||
|     <span> |     <span> | ||||||
|       <div class="blocks-wrapper"> |       <div class="blocks-wrapper"> | ||||||
|         <app-mempool-blocks></app-mempool-blocks> |         <div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div> | ||||||
|         <app-blockchain-blocks></app-blockchain-blocks> |         <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> | ||||||
|       <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> |         <button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button> | ||||||
|       </div> |       </div> | ||||||
|     </span> |     </span> | ||||||
|  | |||||||
| @ -72,6 +72,15 @@ | |||||||
|   position: relative; |   position: relative; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .scroll-spacer { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0; | ||||||
|  |   left: 0; | ||||||
|  |   width: 1px; | ||||||
|  |   height: 1px; | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .loading-block { | .loading-block { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   text-align: center; |   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 { Subscription } from 'rxjs'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| 
 | 
 | ||||||
| @ -9,6 +9,11 @@ import { StateService } from '../../services/state.service'; | |||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class BlockchainComponent implements OnInit, OnDestroy { | export class BlockchainComponent implements OnInit, OnDestroy { | ||||||
|  |   @Input() pages: any[] = []; | ||||||
|  |   @Input() pageIndex: number; | ||||||
|  |   @Input() blocksPerPage: number = 8; | ||||||
|  |   @Input() minScrollWidth: number = 0; | ||||||
|  | 
 | ||||||
|   network: string; |   network: string; | ||||||
|   timeLtrSubscription: Subscription; |   timeLtrSubscription: Subscription; | ||||||
|   timeLtr: boolean = this.stateService.timeLtr.value; |   timeLtr: boolean = this.stateService.timeLtr.value; | ||||||
| @ -29,6 +34,10 @@ export class BlockchainComponent implements OnInit, OnDestroy { | |||||||
|     this.timeLtrSubscription.unsubscribe(); |     this.timeLtrSubscription.unsubscribe(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   trackByPageFn(index: number, item: { index: number }) { | ||||||
|  |     return item.index; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   toggleTimeDirection() { |   toggleTimeDirection() { | ||||||
|     this.ltrTransitionEnabled = true; |     this.ltrTransitionEnabled = true; | ||||||
|     this.stateService.timeLtr.next(!this.timeLtr); |     this.stateService.timeLtr.next(!this.timeLtr); | ||||||
|  | |||||||
| @ -11,8 +11,9 @@ | |||||||
| <div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer | <div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer | ||||||
|   (mousedown)="onMouseDown($event)" |   (mousedown)="onMouseDown($event)" | ||||||
|   (dragstart)="onDragStart($event)" |   (dragstart)="onDragStart($event)" | ||||||
|  |   (scroll)="onScroll($event)" | ||||||
| > | > | ||||||
| <app-blockchain></app-blockchain> |   <app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <router-outlet></router-outlet> | <router-outlet></router-outlet> | ||||||
|  | |||||||
| @ -19,16 +19,51 @@ export class StartComponent implements OnInit, OnDestroy { | |||||||
|   blockchainScrollLeftInit: number; |   blockchainScrollLeftInit: number; | ||||||
|   timeLtrSubscription: Subscription; |   timeLtrSubscription: Subscription; | ||||||
|   timeLtr: boolean = this.stateService.timeLtr.value; |   timeLtr: boolean = this.stateService.timeLtr.value; | ||||||
|  |   chainTipSubscription: Subscription; | ||||||
|  |   chainTip: number = -1; | ||||||
|  |   markBlockSubscription: Subscription; | ||||||
|   @ViewChild('blockchainContainer') blockchainContainer: ElementRef; |   @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( |   constructor( | ||||||
|     private stateService: StateService, |     private stateService: StateService, | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit() { |   ngOnInit() { | ||||||
|  |     this.firstPageWidth = 40 + (this.blockWidth * this.stateService.env.KEEP_BLOCKS_AMOUNT); | ||||||
|  |     this.onResize(); | ||||||
|  |     this.updatePages(); | ||||||
|     this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { |     this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { | ||||||
|       this.timeLtr = !!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$ |     this.stateService.blocks$ | ||||||
|       .subscribe((blocks: any) => { |       .subscribe((blocks: any) => { | ||||||
|         if (this.stateService.network !== '') { |         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) { |   onMouseDown(event: MouseEvent) { | ||||||
|     this.mouseDragStartX = event.clientX; |     this.mouseDragStartX = event.clientX; | ||||||
|     this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; |     this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; | ||||||
| @ -70,7 +133,7 @@ export class StartComponent implements OnInit, OnDestroy { | |||||||
|     if (this.mouseDragStartX != null) { |     if (this.mouseDragStartX != null) { | ||||||
|       this.stateService.setBlockScrollingInProgress(true); |       this.stateService.setBlockScrollingInProgress(true); | ||||||
|       this.blockchainContainer.nativeElement.scrollLeft = |       this.blockchainContainer.nativeElement.scrollLeft = | ||||||
|         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX |         this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   @HostListener('document:mouseup', []) |   @HostListener('document:mouseup', []) | ||||||
| @ -79,7 +142,149 @@ export class StartComponent implements OnInit, OnDestroy { | |||||||
|     this.stateService.setBlockScrollingInProgress(false); |     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() { |   ngOnDestroy() { | ||||||
|     this.timeLtrSubscription.unsubscribe(); |     this.timeLtrSubscription.unsubscribe(); | ||||||
|  |     this.chainTipSubscription.unsubscribe(); | ||||||
|  |     this.markBlockSubscription.unsubscribe(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import { | |||||||
| import { Transaction, Vout } from '../../interfaces/electrs.interface'; | import { Transaction, Vout } from '../../interfaces/electrs.interface'; | ||||||
| import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; | import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
|  | import { CacheService } from '../../services/cache.service'; | ||||||
| import { OpenGraphService } from '../../services/opengraph.service'; | import { OpenGraphService } from '../../services/opengraph.service'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| import { SeoService } from '../../services/seo.service'; | import { SeoService } from '../../services/seo.service'; | ||||||
| @ -45,6 +46,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | |||||||
|     private route: ActivatedRoute, |     private route: ActivatedRoute, | ||||||
|     private electrsApiService: ElectrsApiService, |     private electrsApiService: ElectrsApiService, | ||||||
|     private stateService: StateService, |     private stateService: StateService, | ||||||
|  |     private cacheService: CacheService, | ||||||
|     private apiService: ApiService, |     private apiService: ApiService, | ||||||
|     private seoService: SeoService, |     private seoService: SeoService, | ||||||
|     private openGraphService: OpenGraphService, |     private openGraphService: OpenGraphService, | ||||||
| @ -97,7 +99,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { | |||||||
|         }), |         }), | ||||||
|         switchMap(() => { |         switchMap(() => { | ||||||
|           let transactionObservable$: Observable<Transaction>; |           let transactionObservable$: Observable<Transaction>; | ||||||
|           const cached = this.stateService.getTxFromCache(this.txId); |           const cached = this.cacheService.getTxFromCache(this.txId); | ||||||
|           if (cached && cached.fee !== -1) { |           if (cached && cached.fee !== -1) { | ||||||
|             transactionObservable$ = of(cached); |             transactionObservable$ = of(cached); | ||||||
|           } else { |           } else { | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import { | |||||||
| import { Transaction } from '../../interfaces/electrs.interface'; | import { Transaction } from '../../interfaces/electrs.interface'; | ||||||
| import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; | import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
|  | import { CacheService } from '../../services/cache.service'; | ||||||
| import { WebsocketService } from '../../services/websocket.service'; | import { WebsocketService } from '../../services/websocket.service'; | ||||||
| import { AudioService } from '../../services/audio.service'; | import { AudioService } from '../../services/audio.service'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| @ -74,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|     private relativeUrlPipe: RelativeUrlPipe, |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|     private electrsApiService: ElectrsApiService, |     private electrsApiService: ElectrsApiService, | ||||||
|     private stateService: StateService, |     private stateService: StateService, | ||||||
|  |     private cacheService: CacheService, | ||||||
|     private websocketService: WebsocketService, |     private websocketService: WebsocketService, | ||||||
|     private audioService: AudioService, |     private audioService: AudioService, | ||||||
|     private apiService: ApiService, |     private apiService: ApiService, | ||||||
| @ -197,7 +199,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|         }), |         }), | ||||||
|         switchMap(() => { |         switchMap(() => { | ||||||
|           let transactionObservable$: Observable<Transaction>; |           let transactionObservable$: Observable<Transaction>; | ||||||
|           const cached = this.stateService.getTxFromCache(this.txId); |           const cached = this.cacheService.getTxFromCache(this.txId); | ||||||
|           if (cached && cached.fee !== -1) { |           if (cached && cached.fee !== -1) { | ||||||
|             transactionObservable$ = of(cached); |             transactionObservable$ = of(cached); | ||||||
|           } else { |           } else { | ||||||
| @ -296,7 +298,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | |||||||
|         this.waitingForTransaction = false; |         this.waitingForTransaction = false; | ||||||
|       } |       } | ||||||
|       this.rbfTransaction = rbfTransaction; |       this.rbfTransaction = rbfTransaction; | ||||||
|       this.stateService.setTxCache([this.rbfTransaction]); |       this.cacheService.setTxCache([this.rbfTransaction]); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { |     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; | import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
|  | import { CacheService } from '../../services/cache.service'; | ||||||
| import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; | import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; | ||||||
| import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | ||||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
| @ -44,6 +45,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|  |     private cacheService: CacheService, | ||||||
|     private electrsApiService: ElectrsApiService, |     private electrsApiService: ElectrsApiService, | ||||||
|     private apiService: ApiService, |     private apiService: ApiService, | ||||||
|     private assetsService: AssetsService, |     private assetsService: AssetsService, | ||||||
| @ -123,7 +125,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.transactionsLength = this.transactions.length; |       this.transactionsLength = this.transactions.length; | ||||||
|       this.stateService.setTxCache(this.transactions); |       this.cacheService.setTxCache(this.transactions); | ||||||
| 
 | 
 | ||||||
|       this.transactions.forEach((tx) => { |       this.transactions.forEach((tx) => { | ||||||
|         tx['@voutLimit'] = true; |         tx['@voutLimit'] = true; | ||||||
|  | |||||||
| @ -121,8 +121,6 @@ export interface BlockExtension { | |||||||
|     name: string; |     name: string; | ||||||
|     slug: string; |     slug: string; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   stage?: number; // Frontend only
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface BlockExtended extends Block { | 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); |   backendInfo$ = new ReplaySubject<IBackendInfo>(1); | ||||||
|   loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1); |   loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1); | ||||||
|   recommendedFees$ = new ReplaySubject<Recommendedfees>(1); |   recommendedFees$ = new ReplaySubject<Recommendedfees>(1); | ||||||
|  |   chainTip$ = new ReplaySubject<number>(-1); | ||||||
| 
 | 
 | ||||||
|   live2Chart$ = new Subject<OptimizedMempoolStats>(); |   live2Chart$ = new Subject<OptimizedMempoolStats>(); | ||||||
| 
 | 
 | ||||||
| @ -111,15 +112,13 @@ export class StateService { | |||||||
|   connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); |   connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); | ||||||
|   isTabHidden$: Observable<boolean>; |   isTabHidden$: Observable<boolean>; | ||||||
| 
 | 
 | ||||||
|   markBlock$ = new ReplaySubject<MarkBlockState>(); |   markBlock$ = new BehaviorSubject<MarkBlockState>({}); | ||||||
|   keyNavigation$ = new Subject<KeyboardEvent>(); |   keyNavigation$ = new Subject<KeyboardEvent>(); | ||||||
| 
 | 
 | ||||||
|   blockScrolling$: Subject<boolean> = new Subject<boolean>(); |   blockScrolling$: Subject<boolean> = new Subject<boolean>(); | ||||||
|   timeLtr: BehaviorSubject<boolean>; |   timeLtr: BehaviorSubject<boolean>; | ||||||
|   hideFlow: BehaviorSubject<boolean>; |   hideFlow: BehaviorSubject<boolean>; | ||||||
| 
 | 
 | ||||||
|   txCache: { [txid: string]: Transaction } = {}; |  | ||||||
| 
 |  | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(PLATFORM_ID) private platformId: any, |     @Inject(PLATFORM_ID) private platformId: any, | ||||||
|     @Inject(LOCALE_ID) private locale: string, |     @Inject(LOCALE_ID) private locale: string, | ||||||
| @ -274,18 +273,15 @@ export class StateService { | |||||||
|     return this.network === 'liquid' || this.network === 'liquidtestnet'; |     return this.network === 'liquid' || this.network === 'liquidtestnet'; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setTxCache(transactions) { |   resetChainTip() { | ||||||
|     this.txCache = {}; |     this.latestBlockHeight = -1; | ||||||
|     transactions.forEach(tx => { |     this.chainTip$.next(-1); | ||||||
|       this.txCache[tx.txid] = tx; |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|   | 
 | ||||||
|   getTxFromCache(txid) { |   updateChainTip(height) { | ||||||
|     if (this.txCache && this.txCache[txid]) { |     if (height > this.latestBlockHeight) { | ||||||
|       return this.txCache[txid]; |       this.latestBlockHeight = height; | ||||||
|     } else { |       this.chainTip$.next(height); | ||||||
|       return null; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -70,7 +70,7 @@ export class WebsocketService { | |||||||
|         clearTimeout(this.onlineCheckTimeout); |         clearTimeout(this.onlineCheckTimeout); | ||||||
|         clearTimeout(this.onlineCheckTimeoutTwo); |         clearTimeout(this.onlineCheckTimeoutTwo); | ||||||
| 
 | 
 | ||||||
|         this.stateService.latestBlockHeight = -1; |         this.stateService.resetChainTip(); | ||||||
| 
 | 
 | ||||||
|         this.websocketSubject.complete(); |         this.websocketSubject.complete(); | ||||||
|         this.subscription.unsubscribe(); |         this.subscription.unsubscribe(); | ||||||
| @ -224,12 +224,14 @@ export class WebsocketService { | |||||||
|   handleResponse(response: WebsocketResponse) { |   handleResponse(response: WebsocketResponse) { | ||||||
|     if (response.blocks && response.blocks.length) { |     if (response.blocks && response.blocks.length) { | ||||||
|       const blocks = response.blocks; |       const blocks = response.blocks; | ||||||
|  |       let maxHeight = 0; | ||||||
|       blocks.forEach((block: BlockExtended) => { |       blocks.forEach((block: BlockExtended) => { | ||||||
|         if (block.height > this.stateService.latestBlockHeight) { |         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.blocks$.next([block, false]); | ||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|  |       this.stateService.updateChainTip(maxHeight); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (response.tx) { |     if (response.tx) { | ||||||
| @ -238,7 +240,7 @@ export class WebsocketService { | |||||||
| 
 | 
 | ||||||
|     if (response.block) { |     if (response.block) { | ||||||
|       if (response.block.height > this.stateService.latestBlockHeight) { |       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]); |         this.stateService.blocks$.next([response.block, !!response.txConfirmed]); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user