diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 4eedf5bca..9ca360457 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -1,6 +1,6 @@ const config = require('../../mempool-config.json'); import bitcoinApi from './bitcoin/electrs-api'; -import { MempoolInfo, TransactionExtended, Transaction } from '../interfaces'; +import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond } from '../interfaces'; class Mempool { private inSync: boolean = false; @@ -12,7 +12,7 @@ class Mempool { private txPerSecondArray: number[] = []; private txPerSecond: number = 0; - private vBytesPerSecondArray: any[] = []; + private vBytesPerSecondArray: VbytesPerSecond[] = []; private vBytesPerSecond: number = 0; constructor() { diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index a9500d42b..d525f18c6 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -222,3 +222,8 @@ export interface WebsocketResponse { 'track-address': string; 'watch-mempool': boolean; } + +export interface VbytesPerSecond { + unixTime: number; + vSize: number; +} diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index bfd80a03e..de257f833 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -35,3 +35,4 @@ export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 7 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; export const ELCTRS_ITEMS_PER_PAGE = 25; +export const KEEP_BLOCKS_AMOUNT = 8; diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index b7ed241f1..16deecabf 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,2 +1,2 @@ multisig {{ multisigM }} of {{ multisigN }} -Layer2 Peg-out +Layer{{ network === 'liquid' ? '3' : '2' }} Peg-out diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index e90461cae..e7a48dc07 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; import { Vin, Vout } from '../../interfaces/electrs.interface'; +import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-address-labels', @@ -8,6 +9,7 @@ import { Vin, Vout } from '../../interfaces/electrs.interface'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AddressLabelsComponent implements OnInit { + network = ''; @Input() vin: Vin; @Input() vout: Vout; @@ -18,7 +20,11 @@ export class AddressLabelsComponent implements OnInit { secondLayerClose = false; - constructor() { } + constructor( + stateService: StateService, + ) { + this.network = stateService.network; + } ngOnInit() { if (this.vin) { diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 473a3e43d..60624e250 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -120,7 +120,7 @@ export class BlockComponent implements OnInit, OnDestroy { }); this.stateService.blocks$ - .subscribe((block) => this.latestBlock = block); + .subscribe(([block]) => this.latestBlock = block); this.stateService.networkChanged$ .subscribe((network) => this.network = network); diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index f7b0d223f..8da403914 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -1,6 +1,6 @@
-
+
 
{{ block.height }} diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index 9a72ffebb..a36eeceb7 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -14,7 +14,7 @@ .mined-block { position: absolute; top: 0px; - transition: 1s; + transition: 2s; } .block-size { diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 90e0d274d..730306003 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,8 +1,10 @@ -import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { Block } from 'src/app/interfaces/electrs.interface'; import { StateService } from 'src/app/services/state.service'; import { Router } from '@angular/router'; +import { AudioService } from 'src/app/services/audio.service'; +import { KEEP_BLOCKS_AMOUNT } from 'src/app/app.constants'; @Component({ selector: 'app-blockchain-blocks', @@ -14,6 +16,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { blocks: Block[] = []; markHeight: number; blocksSubscription: Subscription; + blockStyles = []; interval: any; arrowVisible = false; @@ -30,20 +33,40 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { constructor( private stateService: StateService, private router: Router, + private audioService: AudioService, ) { } ngOnInit() { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.blocksSubscription = this.stateService.blocks$ - .subscribe((block) => { + .subscribe(([block, txConfirmed]) => { + const currentBlocksAmount = this.blocks.length; if (this.blocks.some((b) => b.height === block.height)) { return; } this.blocks.unshift(block); this.blocks = this.blocks.slice(0, 8); - this.moveArrowToPosition(true); + if (currentBlocksAmount === KEEP_BLOCKS_AMOUNT) { + setTimeout(() => this.audioService.playSound('bright-harmony')); + block.stage = block.matchRate >= 80 ? 1 : 2; + } + + 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.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); + }, 50); + }); this.stateService.markBlock$ @@ -83,20 +106,28 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { clearInterval(this.interval); } - moveArrowToPosition(animate: boolean) { + moveArrowToPosition(animate: boolean, newBlockFromLeft = false) { if (!this.markHeight) { this.arrowVisible = false; return; } const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); - if (blockindex !== -1) { + if (blockindex > -1) { if (!animate) { this.transition = 'inherit'; } this.arrowVisible = true; - this.arrowLeftPx = blockindex * 155 + 30; - if (!animate) { - setTimeout(() => this.transition = '1s'); + if (newBlockFromLeft) { + this.arrowLeftPx = blockindex * 155 + 30 - 205; + setTimeout(() => { + this.transition = '2s'; + this.arrowLeftPx = blockindex * 155 + 30; + }, 50); + } else { + this.arrowLeftPx = blockindex * 155 + 30; + if (!animate) { + setTimeout(() => this.transition = '2s'); + } } } } @@ -107,8 +138,15 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { getStyleForBlock(block: Block) { const greenBackgroundHeight = 100 - (block.weight / 4000000) * 100; + let addLeft = 0; + + if (block.stage === 1) { + block.stage = 2; + addLeft = -205; + } + return { - left: 155 * this.blocks.indexOf(block) + 'px', + left: addLeft + 155 * this.blocks.indexOf(block) + 'px', background: `repeating-linear-gradient( #2d3348, #2d3348 ${greenBackgroundHeight}%, diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts index 893cce888..1c85e13f3 100644 --- a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts @@ -34,7 +34,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.blockSubscription = this.stateService.blocks$ - .subscribe((block) => { + .subscribe(([block]) => { if (block === null || !this.blocks.length) { return; } diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index b3639716a..897e091b3 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { StateService } from 'src/app/services/state.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { switchMap, map, tap } from 'rxjs/operators'; +import { switchMap, map, tap, filter } from 'rxjs/operators'; import { MempoolBlock } from 'src/app/interfaces/websocket.interface'; import { Observable } from 'rxjs'; import { SeoService } from 'src/app/services/seo.service'; @@ -29,6 +29,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { this.mempoolBlockIndex = parseInt(params.get('id'), 10) || 0; return this.stateService.mempoolBlocks$ .pipe( + filter((mempoolBlocks) => mempoolBlocks.length > 0), map((mempoolBlocks) => { while (!mempoolBlocks[this.mempoolBlockIndex]) { this.mempoolBlockIndex--; diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index 4fea06bc1..27fb4ca45 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -1,6 +1,7 @@ .bitcoin-block { width: 125px; height: 125px; + transition: 2s; } .block-size { diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 2ab086746..ca611dfe9 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -23,13 +23,14 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { arrowVisible = false; rightPosition = 0; - transition = '1s'; + transition = '2s'; markIndex: number; txFeePerVSize: number; resetTransitionTimeout: number; - blocksLeftToHalving: number; + + blockIndex = 1; constructor( private router: Router, @@ -37,14 +38,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { ) { } ngOnInit() { - - this.stateService.blocks$ - .subscribe((block) => { - this.blocksLeftToHalving = 630000 - block.height; - }); - this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$ .subscribe((blocks) => { + blocks.forEach((block, i) => { + block.index = this.blockIndex + i; + }); const stringifiedBlocks = JSON.stringify(blocks); this.mempoolBlocksFull = JSON.parse(stringifiedBlocks); this.mempoolBlocks = this.reduceMempoolBlocksToFitScreen(JSON.parse(stringifiedBlocks)); @@ -65,6 +63,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.calculateTransactionPosition(); }); + this.stateService.blocks$ + .subscribe(([block]) => { + if (block.matchRate >= 80) { + this.blockIndex++; + } + }); + this.stateService.networkChanged$ .subscribe((network) => this.network = network); @@ -79,7 +84,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { } else { this.stateService.blocks$ .pipe(take(8)) - .subscribe((block) => { + .subscribe(([block]) => { if (this.stateService.latestBlockHeight === block.height) { this.router.navigate([(this.network ? '/' + this.network : '') + '/block/', block.id], { state: { data: { block } }}); } @@ -104,8 +109,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.mempoolBlocksSubscription.unsubscribe(); } - trackByFn(index: number) { - return index; + trackByFn(index: number, block: MempoolBlock) { + return block.index; } reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] { @@ -176,7 +181,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.transition = 'inherit'; this.rightPosition = this.markIndex * (this.blockWidth + this.blockPadding) + 0.5 * this.blockWidth; this.arrowVisible = true; - this.resetTransitionTimeout = window.setTimeout(() => this.transition = '1s', 100); + this.resetTransitionTimeout = window.setTimeout(() => this.transition = '2s', 100); return; } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 4568e4cee..be349330e 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -113,19 +113,20 @@ export class TransactionComponent implements OnInit, OnDestroy { }); this.stateService.blocks$ - .subscribe((block) => this.latestBlock = block); + .subscribe(([block, txConfirmed]) => { + this.latestBlock = block; - this.stateService.txConfirmed$ - .subscribe((block) => { - this.tx.status = { - confirmed: true, - block_height: block.height, - block_hash: block.id, - block_time: block.timestamp, - }; - this.stateService.markBlock$.next({ blockHeight: block.height }); - this.audioService.playSound('magic'); - this.findBlockAndSetFeeRating(); + if (txConfirmed) { + this.tx.status = { + confirmed: true, + block_height: block.height, + block_hash: block.id, + block_time: block.timestamp, + }; + this.stateService.markBlock$.next({ blockHeight: block.height }); + this.audioService.playSound('magic'); + this.findBlockAndSetFeeRating(); + } }); this.stateService.txReplaced$ @@ -171,10 +172,10 @@ export class TransactionComponent implements OnInit, OnDestroy { findBlockAndSetFeeRating() { this.stateService.blocks$ .pipe( - filter((block) => block.height === this.tx.status.block_height), + filter(([block]) => block.height === this.tx.status.block_height), take(1) ) - .subscribe((block) => { + .subscribe(([block]) => { const feePervByte = this.tx.fee / (this.tx.weight / 4); this.medianFeeNeeded = Math.round(block.feeRange[Math.round(block.feeRange.length * 0.5)]); diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 6cb4e29dd..115de55c9 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -125,7 +125,7 @@
- + diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index b861920fe..2fc0fbe67 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -22,7 +22,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { @Output() loadMore = new EventEmitter(); - latestBlock$: Observable; + latestBlock: Block; outspends: Outspend[] = []; assetsMinimal: any; @@ -34,7 +34,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { ) { } ngOnInit() { - this.latestBlock$ = this.stateService.blocks$; + this.stateService.blocks$.subscribe(([block]) => this.latestBlock = block); this.stateService.networkChanged$.subscribe((network) => this.network = network); if (this.network === 'liquid') { diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index f939ce091..20b64a2a5 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -89,10 +89,13 @@ export interface Block { merkle_root: string; previousblockhash: string; + // Custom properties medianFee?: number; feeRange?: number[]; reward?: number; coinbaseTx?: Transaction; + matchRate: number; + stage: number; } export interface Address { diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 6c59a4b6b..abab7acf0 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -25,6 +25,7 @@ export interface MempoolBlock { medianFee: number; totalFees: number; feeRange: number[]; + index: number; } export interface MemPoolState { diff --git a/frontend/src/app/services/audio.service.ts b/frontend/src/app/services/audio.service.ts index 29c14819c..4b3060b24 100644 --- a/frontend/src/app/services/audio.service.ts +++ b/frontend/src/app/services/audio.service.ts @@ -5,17 +5,20 @@ import { Injectable } from '@angular/core'; }) export class AudioService { audio = new Audio(); + isPlaying = false; constructor() { } - public playSound(name: 'magic' | 'chime' | 'cha-ching') { - try { - this.audio.src = '../../../resources/sounds/' + name + '.mp3'; - this.audio.load(); - this.audio.play(); - } catch (e) { - console.log('Play sound failed', e); + public playSound(name: 'magic' | 'chime' | 'cha-ching' | 'bright-harmony') { + if (this.isPlaying) { + return; } + this.isPlaying = true; + this.audio.src = '../../../resources/sounds/' + name + '.mp3'; + this.audio.load(); + this.audio.play().catch((e) => { + console.log('Play sound failed', e); + }); + setTimeout(() => this.isPlaying = false, 100); } - } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 4cd6a72ea..46bb37f18 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -4,6 +4,7 @@ import { Block, Transaction } from '../interfaces/electrs.interface'; import { MempoolBlock, MemPoolState } from '../interfaces/websocket.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; +import { KEEP_BLOCKS_AMOUNT } from '../app.constants'; interface MarkBlockState { blockHeight?: number; @@ -19,11 +20,10 @@ export class StateService { latestBlockHeight = 0; networkChanged$ = new ReplaySubject(1); - blocks$ = new ReplaySubject(8); + blocks$ = new ReplaySubject<[Block, boolean]>(KEEP_BLOCKS_AMOUNT); conversions$ = new ReplaySubject(1); mempoolStats$ = new ReplaySubject(1); mempoolBlocks$ = new ReplaySubject(1); - txConfirmed$ = new Subject(); txReplaced$ = new Subject(); mempoolTransactions$ = new Subject(); blockTransactions$ = new Subject(); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 65846df7f..00d89ba44 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -64,7 +64,7 @@ export class WebsocketService { blocks.forEach((block: Block) => { if (block.height > this.stateService.latestBlockHeight) { this.stateService.latestBlockHeight = block.height; - this.stateService.blocks$.next(block); + this.stateService.blocks$.next([block, false]); } }); } @@ -76,12 +76,11 @@ export class WebsocketService { if (response.block) { if (response.block.height > this.stateService.latestBlockHeight) { this.stateService.latestBlockHeight = response.block.height; - this.stateService.blocks$.next(response.block); + this.stateService.blocks$.next([response.block, !!response.txConfirmed]); } if (response.txConfirmed) { this.isTrackingTx = false; - this.stateService.txConfirmed$.next(response.block); } } diff --git a/frontend/src/resources/sounds/bright-harmony.mp3 b/frontend/src/resources/sounds/bright-harmony.mp3 new file mode 100644 index 000000000..5a0435e0a Binary files /dev/null and b/frontend/src/resources/sounds/bright-harmony.mp3 differ