diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 6346c837c..98ad9de95 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -24,7 +24,7 @@ export class AppComponent implements OnInit { txId: ['', Validators.pattern('^[a-fA-F0-9]{64}$')], }); - this.memPoolService.isOffline + this.memPoolService.isOffline$ .subscribe((state) => { this.isOffline = state; }); @@ -42,7 +42,7 @@ export class AppComponent implements OnInit { } else { this.router.navigate(['/tx/', txId]); } - this.memPoolService.txIdSearch.next(txId); + this.memPoolService.txIdSearch$.next(txId); this.searchForm.setValue({ txId: '', }); diff --git a/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts b/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts index fe2bf5da6..b38874f17 100644 --- a/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts +++ b/frontend/src/app/blockchain-blocks/block-modal/block-modal.component.ts @@ -56,7 +56,7 @@ export class BlockModalComponent implements OnInit { ] }; - this.memPoolService.conversions + this.memPoolService.conversions$ .subscribe((conversions) => { this.conversions = conversions; }); diff --git a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts index 61b34ebdc..aef5b4c7e 100644 --- a/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/blockchain-blocks/blockchain-blocks.component.ts @@ -1,21 +1,33 @@ -import { Component, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { IBlock } from '../blockchain/interfaces'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { BlockModalComponent } from './block-modal/block-modal.component'; +import { MemPoolService } from '../services/mem-pool.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-blockchain-blocks', templateUrl: './blockchain-blocks.component.html', styleUrls: ['./blockchain-blocks.component.scss'] }) -export class BlockchainBlocksComponent { - - @Input() blocks: IBlock[]; +export class BlockchainBlocksComponent implements OnInit, OnDestroy { + blocks: IBlock[] = []; + blocksSubscription: Subscription; constructor( private modalService: NgbModal, + private memPoolService: MemPoolService, ) { } + ngOnInit() { + this.blocksSubscription = this.memPoolService.blocks$ + .subscribe((block) => this.blocks.unshift(block)); + } + + ngOnDestroy() { + this.blocksSubscription.unsubscribe(); + } + getTimeSinceMined(block: IBlock): string { const minutes = ((new Date().getTime()) - (new Date(block.time * 1000).getTime())) / 1000 / 60; if (minutes >= 120) { diff --git a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss index 489fe8628..dc5ce18c4 100644 --- a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss +++ b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.scss @@ -9,12 +9,6 @@ font-weight: bold; } -.blocks-container { - position: absolute; - top: 0px; - left: 40px; -} - .projected-blocks-container { position: absolute; top: 0px; diff --git a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts index ff6f3bfac..0bfe5fc31 100644 --- a/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts +++ b/frontend/src/app/blockchain-projected-blocks/blockchain-projected-blocks.component.ts @@ -1,21 +1,33 @@ -import { Component, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { IProjectedBlock, IBlock } from '../blockchain/interfaces'; import { ProjectedBlockModalComponent } from './projected-block-modal/projected-block-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { MemPoolService } from '../services/mem-pool.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-blockchain-projected-blocks', templateUrl: './blockchain-projected-blocks.component.html', styleUrls: ['./blockchain-projected-blocks.component.scss'] }) -export class BlockchainProjectedBlocksComponent { - - @Input() projectedBlocks: IProjectedBlock[]; +export class BlockchainProjectedBlocksComponent implements OnInit, OnDestroy { + projectedBlocks: IProjectedBlock[]; + subscription: Subscription; constructor( private modalService: NgbModal, + private memPoolService: MemPoolService, ) { } + ngOnInit() { + this.subscription = this.memPoolService.projectedBlocks$ + .subscribe((projectedblocks) => this.projectedBlocks = projectedblocks); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + trackByProjectedFn(index: number) { return index; } diff --git a/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts b/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts index d66340337..3bf267b77 100644 --- a/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts +++ b/frontend/src/app/blockchain-projected-blocks/projected-block-modal/projected-block-modal.component.ts @@ -57,7 +57,7 @@ export class ProjectedBlockModalComponent implements OnInit { ] }; - this.memPoolService.conversions + this.memPoolService.conversions$ .subscribe((conversions) => { this.conversions = conversions; }); diff --git a/frontend/src/app/blockchain/blockchain.component.html b/frontend/src/app/blockchain/blockchain.component.html index f18cb433c..7f73b2a3e 100644 --- a/frontend/src/app/blockchain/blockchain.component.html +++ b/frontend/src/app/blockchain/blockchain.component.html @@ -1,9 +1,9 @@ -
+

Loading blocks...


-
+

Locating transaction...

@@ -11,15 +11,14 @@
+ + - - - -
+
- + diff --git a/frontend/src/app/blockchain/blockchain.component.ts b/frontend/src/app/blockchain/blockchain.component.ts index 2a906426a..a58f943f3 100644 --- a/frontend/src/app/blockchain/blockchain.component.ts +++ b/frontend/src/app/blockchain/blockchain.component.ts @@ -1,9 +1,9 @@ -import { Component, OnInit, OnDestroy, Renderer2, HostListener } from '@angular/core'; -import { IMempoolDefaultResponse, IBlock, IProjectedBlock, ITransaction } from './interfaces'; -import { retryWhen, tap } from 'rxjs/operators'; -import { MemPoolService } from '../services/mem-pool.service'; +import { Component, OnInit, OnDestroy, Renderer2 } from '@angular/core'; +import { MemPoolService, ITxTracking } from '../services/mem-pool.service'; import { ApiService } from '../services/api.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; @Component({ selector: 'app-blockchain', @@ -11,23 +11,12 @@ import { ActivatedRoute, ParamMap } from '@angular/router'; styleUrls: ['./blockchain.component.scss'] }) export class BlockchainComponent implements OnInit, OnDestroy { - blocks: IBlock[] = []; - projectedBlocks: IProjectedBlock[] = []; - subscription: any; - socket: any; - txBubbleStyle: any = {}; + txTrackingSubscription: Subscription; + blocksSubscription: Subscription; txTrackingLoading = false; - txTrackingEnabled = false; - txTrackingTx: ITransaction | null = null; - txTrackingBlockHeight = 0; txShowTxNotFound = false; - txBubbleArrowPosition = 'top'; - - @HostListener('window:resize', ['$event']) - onResize(event: Event) { - this.moveTxBubbleToPosition(); - } + isLoading = true; constructor( private memPoolService: MemPoolService, @@ -37,72 +26,15 @@ export class BlockchainComponent implements OnInit, OnDestroy { ) {} ngOnInit() { + this.txTrackingSubscription = this.memPoolService.txTracking$ + .subscribe((response: ITxTracking) => { + this.txTrackingLoading = false; + this.txShowTxNotFound = response.notFound; + if (this.txShowTxNotFound) { + setTimeout(() => { this.txShowTxNotFound = false; }, 2000); + } + }); - this.txBubbleStyle = { - 'position': 'absolute', - 'top': '425px', - 'visibility': 'hidden', - }; - this.socket = this.apiService.websocketSubject; - this.subscription = this.socket - .pipe( - retryWhen((errors: any) => errors.pipe( - tap(() => this.memPoolService.isOffline.next(true)))) - ) - .subscribe((response: IMempoolDefaultResponse) => { - this.memPoolService.isOffline.next(false); - if (response.mempoolInfo && response.txPerSecond !== undefined) { - this.memPoolService.loaderSubject.next({ - memPoolInfo: response.mempoolInfo, - txPerSecond: response.txPerSecond, - vBytesPerSecond: response.vBytesPerSecond, - }); - } - if (response.blocks && response.blocks.length) { - this.blocks = response.blocks; - this.blocks.reverse(); - } - if (response.block) { - if (!this.blocks.some((block) => response.block !== undefined && response.block.height === block.height )) { - this.blocks.unshift(response.block); - if (this.blocks.length >= 8) { - this.blocks.pop(); - } - } - } - if (response.conversions) { - this.memPoolService.conversions.next(response.conversions); - } - if (response.projectedBlocks) { - this.projectedBlocks = response.projectedBlocks; - const mempoolWeight = this.projectedBlocks.map((block) => block.blockWeight).reduce((a, b) => a + b); - this.memPoolService.mempoolWeight.next(mempoolWeight); - } - if (response['track-tx']) { - if (response['track-tx'].tracking) { - this.txTrackingEnabled = true; - this.txTrackingBlockHeight = response['track-tx'].blockHeight; - if (response['track-tx'].tx) { - this.txTrackingTx = response['track-tx'].tx; - this.txTrackingLoading = false; - } - } else { - this.txTrackingEnabled = false; - this.txTrackingTx = null; - this.txTrackingBlockHeight = 0; - } - if (response['track-tx'].message && response['track-tx'].message === 'not-found') { - this.txTrackingLoading = false; - this.txShowTxNotFound = true; - setTimeout(() => { this.txShowTxNotFound = false; }, 2000); - } - setTimeout(() => { - this.moveTxBubbleToPosition(); - }); - } - }, - (err: Error) => console.log(err) - ); this.renderer.addClass(document.body, 'disable-scroll'); this.route.paramMap @@ -112,73 +44,27 @@ export class BlockchainComponent implements OnInit, OnDestroy { return; } this.txTrackingLoading = true; - this.socket.next({'action': 'track-tx', 'txId': txId}); + this.apiService.sendWebSocket({'action': 'track-tx', 'txId': txId}); }); - this.memPoolService.txIdSearch + this.memPoolService.txIdSearch$ .subscribe((txId) => { if (txId) { this.txTrackingLoading = true; - this.socket.next({'action': 'track-tx', 'txId': txId}); + this.apiService.sendWebSocket({'action': 'track-tx', 'txId': txId}); } }); - } - moveTxBubbleToPosition() { - let element: HTMLElement | null = null; - if (this.txTrackingBlockHeight === 0) { - const index = this.projectedBlocks.findIndex((pB) => pB.hasMytx); - if (index > -1) { - element = document.getElementById('projected-block-' + index); - } else { - return; - } - } else { - element = document.getElementById('bitcoin-block-' + this.txTrackingBlockHeight); - } - - this.txBubbleStyle['visibility'] = 'visible'; - this.txBubbleStyle['position'] = 'absolute'; - - if (!element) { - if (window.innerWidth <= 768) { - this.txBubbleArrowPosition = 'bottom'; - this.txBubbleStyle['left'] = window.innerWidth / 2 - 50 + 'px'; - this.txBubbleStyle['bottom'] = '270px'; - this.txBubbleStyle['top'] = 'inherit'; - this.txBubbleStyle['position'] = 'fixed'; - } else { - this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; - this.txBubbleArrowPosition = 'right'; - this.txBubbleStyle['top'] = '425px'; - } - } else { - this.txBubbleArrowPosition = 'top'; - const domRect: DOMRect | ClientRect = element.getBoundingClientRect(); - this.txBubbleStyle['left'] = domRect.left - 50 + 'px'; - this.txBubbleStyle['top'] = domRect.top + 125 + window.scrollY + 'px'; - - if (domRect.left + 100 > window.innerWidth) { - this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; - this.txBubbleArrowPosition = 'right'; - } else if (domRect.left + 220 > window.innerWidth) { - this.txBubbleStyle['left'] = window.innerWidth - 240 + 'px'; - this.txBubbleArrowPosition = 'top-right'; - } else { - this.txBubbleStyle['left'] = domRect.left + 15 + 'px'; - } - - if (domRect.left < 86) { - this.txBubbleArrowPosition = 'top-left'; - this.txBubbleStyle['left'] = 125 + 'px'; - } - } + this.blocksSubscription = this.memPoolService.blocks$ + .pipe( + take(1) + ) + .subscribe((block) => this.isLoading = false); } ngOnDestroy() { - if (this.subscription) { - this.subscription.unsubscribe(); - } + this.blocksSubscription.unsubscribe(); + this.txTrackingSubscription.unsubscribe(); this.renderer.removeClass(document.body, 'disable-scroll'); } } diff --git a/frontend/src/app/footer/footer.component.ts b/frontend/src/app/footer/footer.component.ts index 898995f30..e5bcfd100 100644 --- a/frontend/src/app/footer/footer.component.ts +++ b/frontend/src/app/footer/footer.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { MemPoolService, MemPoolState } from '../services/mem-pool.service'; +import { MemPoolService, IMemPoolState } from '../services/mem-pool.service'; @Component({ selector: 'app-footer', @@ -7,7 +7,7 @@ import { MemPoolService, MemPoolState } from '../services/mem-pool.service'; styleUrls: ['./footer.component.scss'] }) export class FooterComponent implements OnInit { - memPoolInfo: MemPoolState | undefined; + memPoolInfo: IMemPoolState | undefined; mempoolBlocks = 0; progressWidth = ''; progressClass: string; @@ -17,12 +17,12 @@ export class FooterComponent implements OnInit { ) { } ngOnInit() { - this.memPoolService.loaderSubject + this.memPoolService.mempoolStats$ .subscribe((mempoolState) => { this.memPoolInfo = mempoolState; this.updateProgress(); }); - this.memPoolService.mempoolWeight + this.memPoolService.mempoolWeight$ .subscribe((mempoolWeight) => { this.mempoolBlocks = Math.ceil(mempoolWeight / 4000000); }); diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 6ca66def1..dcac10385 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; -import { environment } from '../../environments/environment'; import { webSocket } from 'rxjs/webSocket'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { IMempoolDefaultResponse, IMempoolStats, IBlockTransaction } from '../blockchain/interfaces'; +import { IMempoolDefaultResponse, IMempoolStats, IBlockTransaction, IBlock } from '../blockchain/interfaces'; import { Observable } from 'rxjs'; +import { MemPoolService } from './mem-pool.service'; +import { tap, retryWhen } from 'rxjs/operators'; const WEB_SOCKET_URL = 'ws://' + document.location.hostname + ':8999'; const API_BASE_URL = '/api/v1'; @@ -12,11 +13,95 @@ const API_BASE_URL = '/api/v1'; providedIn: 'root' }) export class ApiService { + private websocketSubject: Observable = webSocket(WEB_SOCKET_URL) + constructor( private httpClient: HttpClient, - ) { } + private memPoolService: MemPoolService, + ) { + this.startSubscription(); + } - websocketSubject = webSocket(WEB_SOCKET_URL); + startSubscription() { + this.websocketSubject + .pipe( + retryWhen((errors: any) => errors + .pipe( + tap(() => this.memPoolService.isOffline$.next(true)) + ) + ), + ) + .subscribe((response: IMempoolDefaultResponse) => { + this.memPoolService.isOffline$.next(false); + + if (response.blocks && response.blocks.length) { + const blocks = response.blocks; + // blocks.reverse(); + blocks.forEach((block: IBlock) => this.memPoolService.blocks$.next(block)); + } + if (response.block) { + this.memPoolService.blocks$.next(response.block); + } + + if (response.projectedBlocks) { + this.memPoolService.projectedBlocks$.next(response.projectedBlocks); + } + + if (response.mempoolInfo && response.txPerSecond !== undefined) { + this.memPoolService.mempoolStats$.next({ + memPoolInfo: response.mempoolInfo, + txPerSecond: response.txPerSecond, + vBytesPerSecond: response.vBytesPerSecond, + }); + } + + if (response.conversions) { + this.memPoolService.conversions$.next(response.conversions); + } + + if (response.projectedBlocks) { + const mempoolWeight = response.projectedBlocks.map((block: any) => block.blockWeight).reduce((a: any, b: any) => a + b); + this.memPoolService.mempoolWeight$.next(mempoolWeight); + } + + if (response['track-tx']) { + let txTrackingEnabled; + let txTrackingBlockHeight; + let txTrackingTx = null; + let txShowTxNotFound = false; + if (response['track-tx'].tracking) { + txTrackingEnabled = true; + txTrackingBlockHeight = response['track-tx'].blockHeight; + if (response['track-tx'].tx) { + txTrackingTx = response['track-tx'].tx; + } + } else { + txTrackingEnabled = false; + txTrackingTx = null; + txTrackingBlockHeight = 0; + } + if (response['track-tx'].message && response['track-tx'].message === 'not-found') { + txShowTxNotFound = true; + } + this.memPoolService.txTracking$.next({ + enabled: txTrackingEnabled, + tx: txTrackingTx, + blockHeight: txTrackingBlockHeight, + notFound: txShowTxNotFound, + }); + } + }), + (err: Error) => { + console.log(err); + console.log('Error, retrying in 10 sec'); + setTimeout(() => this.startSubscription(), 10000); + }; + } + + sendWebSocket(data: any) { + // @ts-ignore + this.websocketSubject.next(data); + } listTransactionsForBlock$(height: number): Observable { return this.httpClient.get(API_BASE_URL + '/transactions/height/' + height); diff --git a/frontend/src/app/services/mem-pool.service.ts b/frontend/src/app/services/mem-pool.service.ts index b792a6cda..60a3939ae 100644 --- a/frontend/src/app/services/mem-pool.service.ts +++ b/frontend/src/app/services/mem-pool.service.ts @@ -1,20 +1,35 @@ import { Injectable } from '@angular/core'; -import { Subject, ReplaySubject } from 'rxjs'; -import { IMempoolInfo } from '../blockchain/interfaces'; +import { ReplaySubject, BehaviorSubject } from 'rxjs'; +import { IMempoolInfo, IBlock, IProjectedBlock, ITransaction } from '../blockchain/interfaces'; -export interface MemPoolState { +export interface IMemPoolState { memPoolInfo: IMempoolInfo; txPerSecond: number; vBytesPerSecond: number; } +export interface ITxTracking { + enabled: boolean; + tx: ITransaction | null; + blockHeight: number; + notFound: boolean; +} + @Injectable({ providedIn: 'root' }) export class MemPoolService { - loaderSubject = new Subject(); - isOffline = new Subject(); - txIdSearch = new Subject(); - conversions = new ReplaySubject(); - mempoolWeight = new Subject(); + mempoolStats$ = new ReplaySubject(); + isOffline$ = new ReplaySubject(); + txIdSearch$ = new ReplaySubject(); + conversions$ = new ReplaySubject(); + mempoolWeight$ = new ReplaySubject(); + txTracking$ = new BehaviorSubject({ + enabled: false, + tx: null, + blockHeight: 0, + notFound: false, + }); + blocks$ = new ReplaySubject(8); + projectedBlocks$ = new BehaviorSubject([]); } diff --git a/frontend/src/app/tx-bubble/tx-bubble.component.html b/frontend/src/app/tx-bubble/tx-bubble.component.html index 612a6de6b..b20dd728f 100644 --- a/frontend/src/app/tx-bubble/tx-bubble.component.html +++ b/frontend/src/app/tx-bubble/tx-bubble.component.html @@ -1,4 +1,4 @@ -
+
@@ -6,8 +6,8 @@ - - + + diff --git a/frontend/src/app/tx-bubble/tx-bubble.component.scss b/frontend/src/app/tx-bubble/tx-bubble.component.scss index 4c759c4c1..105c30991 100644 --- a/frontend/src/app/tx-bubble/tx-bubble.component.scss +++ b/frontend/src/app/tx-bubble/tx-bubble.component.scss @@ -63,3 +63,7 @@ .txBubble .arrow-top-left.txBubbleText::after { left: 20%; } + +.green-color { + color: #3bcc49; +} diff --git a/frontend/src/app/tx-bubble/tx-bubble.component.ts b/frontend/src/app/tx-bubble/tx-bubble.component.ts index 6e5b86c42..92cf2f0a9 100644 --- a/frontend/src/app/tx-bubble/tx-bubble.component.ts +++ b/frontend/src/app/tx-bubble/tx-bubble.component.ts @@ -1,26 +1,144 @@ -import { Component, OnInit, Input, OnChanges } from '@angular/core'; -import { ITransaction } from '../blockchain/interfaces'; +import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; +import { ITransaction, IProjectedBlock } from '../blockchain/interfaces'; +import { Subscription } from 'rxjs'; +import { ITxTracking, MemPoolService } from '../services/mem-pool.service'; @Component({ selector: 'app-tx-bubble', templateUrl: './tx-bubble.component.html', styleUrls: ['./tx-bubble.component.scss'] }) -export class TxBubbleComponent implements OnChanges { - @Input() tx: ITransaction | null = null; - @Input() txTrackingBlockHeight = 0; - @Input() latestBlockHeight = 0; - @Input() arrowPosition: 'top' | 'right' | 'bottom' | 'top-right' | 'top-left' = 'top'; +export class TxBubbleComponent implements OnInit, OnDestroy { + tx: ITransaction | null = null; + txTrackingBlockHeight = 0; + latestBlockHeight = 0; + arrowPosition: 'top' | 'right' | 'bottom' | 'top-right' | 'top-left' = 'top'; + + txTrackingSubscription: Subscription; + projectedBlocksSubscription: Subscription; + blocksSubscription: Subscription; + + projectedBlocks: IProjectedBlock[] = []; txIdShort = ''; confirmations = 0; + conversions: any; - constructor() { } + txBubbleStyle: any = { + 'position': 'absolute', + 'top': '425px', + 'visibility': 'hidden', + }; - ngOnChanges() { - if (this.tx) { - this.txIdShort = this.tx.txid.substring(0, 6) + '...' + this.tx.txid.substring(this.tx.txid.length - 6); + txTrackingLoading = false; + txTrackingEnabled = false; + txTrackingTx: ITransaction | null = null; + txShowTxNotFound = false; + + txBubbleArrowPosition = 'top'; + + @HostListener('window:resize', ['$event']) + onResize(event: Event) { + this.moveTxBubbleToPosition(); + } + + constructor( + private memPoolService: MemPoolService, + ) { } + + ngOnInit() { + this.txTrackingSubscription = this.memPoolService.txTracking$ + .subscribe((response: ITxTracking) => { + this.txTrackingBlockHeight = response.blockHeight; + this.txTrackingEnabled = response.enabled; + if (response.tx) { + this.tx = response.tx; + } + if (this.txTrackingEnabled) { + setTimeout(() => this.moveTxBubbleToPosition()); + } + if (this.txShowTxNotFound) { + setTimeout(() => { this.txShowTxNotFound = false; }, 2000); + } + if (this.tx) { + this.txIdShort = this.tx.txid.substring(0, 6) + '...' + this.tx.txid.substring(this.tx.txid.length - 6); + } + if (this.latestBlockHeight) { + this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; + } + }); + + this.projectedBlocksSubscription = this.memPoolService.projectedBlocks$ + .subscribe((projectedblocks) => this.projectedBlocks = projectedblocks); + + this.blocksSubscription = this.memPoolService.blocks$ + .subscribe((block) => { + this.latestBlockHeight = block.height; + if (this.txTrackingBlockHeight) { + this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; + } + }); + + this.memPoolService.conversions$ + .subscribe((conversions) => { + this.conversions = conversions; + }); + } + + ngOnDestroy() { + this.projectedBlocksSubscription.unsubscribe(); + this.txTrackingSubscription.unsubscribe(); + this.blocksSubscription.unsubscribe(); + } + + moveTxBubbleToPosition() { + let element: HTMLElement | null = null; + if (this.txTrackingBlockHeight === 0) { + const index = this.projectedBlocks.findIndex((pB) => pB.hasMytx); + if (index > -1) { + element = document.getElementById('projected-block-' + index); + } else { + return; + } + } else { + element = document.getElementById('bitcoin-block-' + this.txTrackingBlockHeight); + } + + this.txBubbleStyle['visibility'] = 'visible'; + this.txBubbleStyle['position'] = 'absolute'; + + if (!element) { + if (window.innerWidth <= 768) { + this.txBubbleArrowPosition = 'bottom'; + this.txBubbleStyle['left'] = window.innerWidth / 2 - 50 + 'px'; + this.txBubbleStyle['bottom'] = '270px'; + this.txBubbleStyle['top'] = 'inherit'; + this.txBubbleStyle['position'] = 'fixed'; + } else { + this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; + this.txBubbleArrowPosition = 'right'; + this.txBubbleStyle['top'] = '425px'; + } + } else { + this.txBubbleArrowPosition = 'top'; + const domRect: DOMRect | ClientRect = element.getBoundingClientRect(); + this.txBubbleStyle['left'] = domRect.left - 50 + 'px'; + this.txBubbleStyle['top'] = domRect.top + 140 + window.scrollY + 'px'; + + if (domRect.left + 100 > window.innerWidth) { + this.txBubbleStyle['left'] = window.innerWidth - 220 + 'px'; + this.txBubbleArrowPosition = 'right'; + } else if (domRect.left + 220 > window.innerWidth) { + this.txBubbleStyle['left'] = window.innerWidth - 240 + 'px'; + this.txBubbleArrowPosition = 'top-right'; + } else { + this.txBubbleStyle['left'] = domRect.left + 15 + 'px'; + } + + if (domRect.left < 86) { + this.txBubbleArrowPosition = 'top-left'; + this.txBubbleStyle['left'] = 125 + 'px'; + } } - this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1; } }
{{ txIdShort }}
Fees:{{ tx?.fee }} BTCFee:{{ tx?.fee * 100000000 | number }} sats ({{ conversions.USD * tx?.fee | currency:'USD':'symbol':'1.2-2' }})
Fee per vByte: