diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 2a047472e..718b763c4 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -3,7 +3,8 @@ import * as WebSocket from 'ws'; import { BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, - MempoolDelta, MempoolDeltaTxids + MempoolDelta, MempoolDeltaTxids, + TransactionCompressed } from '../mempool.interfaces'; import blocks from './blocks'; import memPool from './mempool'; @@ -315,6 +316,7 @@ class WebsocketHandler { if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) { if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) { + client['track-mempool-blocks'] = undefined; const index = parsedMessage['track-mempool-block']; client['track-mempool-block'] = index; const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); @@ -324,7 +326,31 @@ class WebsocketHandler { blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx), }); } else { - client['track-mempool-block'] = null; + client['track-mempool-block'] = undefined; + } + } + + if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) { + if (parsedMessage['track-mempool-blocks'].length > 0) { + client['track-mempool-block'] = undefined; + const indices: number[] = []; + const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); + const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = []; + for (const i of parsedMessage['track-mempool-blocks']) { + const index = parseInt(i); + if (Number.isInteger(index) && index >= 0) { + indices.push(index); + updates.push({ + index: index, + sequence: this.mempoolSequence, + blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx), + }); + } + } + client['track-mempool-blocks'] = indices; + response['projected-block-transactions'] = JSON.stringify(updates); + } else { + client['track-mempool-blocks'] = undefined; } } @@ -908,6 +934,19 @@ class WebsocketHandler { delta: mBlockDeltas[index], }); } + } else if (client['track-mempool-blocks']?.length && memPool.isInSync()) { + const indices = client['track-mempool-blocks']; + const updates: string[] = []; + for (const index of indices) { + if (mBlockDeltas[index]) { + updates.push(getCachedResponse(`projected-block-transactions-${index}`, { + index: index, + sequence: this.mempoolSequence, + delta: mBlockDeltas[index], + })); + } + } + response['projected-block-transactions'] = '[' + updates.join(',') + ']'; } if (client['track-rbf'] === 'all' && rbfReplacements) { @@ -1296,6 +1335,27 @@ class WebsocketHandler { }); } } + } else if (client['track-mempool-blocks']?.length && memPool.isInSync()) { + const indices = client['track-mempool-blocks']; + const updates: string[] = []; + for (const index of indices) { + if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) { + if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) { + updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, { + index: index, + sequence: this.mempoolSequence, + blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx), + })); + } else { + updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, { + index: index, + sequence: this.mempoolSequence, + delta: mBlockDeltas[index], + })); + } + } + } + response['projected-block-transactions'] = '[' + updates.join(',') + ']'; } if (client['track-mempool-txids']) { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1f2e3f531..8bed06a9d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router'; import { AppPreloadingStrategy } from './app.preloading-strategy' import { BlockViewComponent } from './components/block-view/block-view.component'; import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; +import { EightMempoolComponent } from './components/eight-mempool/eight-mempool.component'; import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { ClockComponent } from './components/clock/clock.component'; import { StatusViewComponent } from './components/status-view/status-view.component'; @@ -205,6 +206,10 @@ let routes: Routes = [ path: 'view/blocks', component: EightBlocksComponent, }, + { + path: 'view/mempool-blocks', + component: EightMempoolComponent, + }, { path: 'status', data: { networks: ['bitcoin', 'liquid'] }, diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index e16370d96..74a57a8eb 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -278,7 +278,7 @@ export default class BlockScene { } private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void { - this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxY: this.y + this.height })); + this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x - this.width, maxX: this.x + this.width + this.width, maxY: this.y + this.height })); } private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void { diff --git a/frontend/src/app/components/block-overview-graph/tx-sprite.ts b/frontend/src/app/components/block-overview-graph/tx-sprite.ts index 19e0f5834..79525930b 100644 --- a/frontend/src/app/components/block-overview-graph/tx-sprite.ts +++ b/frontend/src/app/components/block-overview-graph/tx-sprite.ts @@ -17,10 +17,11 @@ export default class TxSprite { tempAttributes: OptionalAttributes; minX: number; + maxX: number; maxY: number; - constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX, maxY: number) { + constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, maxY: number) { const offsetTime = params.start; this.vertexArray = vertexArray; this.vertexData = Array(VI.length).fill(0); @@ -30,6 +31,7 @@ export default class TxSprite { }; this.minX = minX; + this.maxX = maxX; this.maxY = maxY; this.attributes = { @@ -84,7 +86,7 @@ export default class TxSprite { minDuration: minimum remaining transition duration when adjust = true temp: if true, this update is only temporary (can be reversed with 'resume') */ - update(params: SpriteUpdateParams, minX?: number, maxY?: number): void { + update(params: SpriteUpdateParams, minX?: number, maxX?: number, maxY?: number): void { const offsetTime = params.start || performance.now(); const v = params.duration > 0 ? (1 / params.duration) : 0; diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index cdce6ec6c..8607cca93 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped { returns minimum transition end time */ - update(params: ViewUpdateParams, { minX, maxY }: { minX: number, maxY: number }): number { + update(params: ViewUpdateParams, { minX, maxX, maxY }: { minX: number, maxX: number, maxY: number }): number { if (params.jitter) { params.delay += (Math.random() * params.jitter); } @@ -117,6 +117,7 @@ export default class TxView implements TransactionStripped { toSpriteUpdate(params), this.vertexArray, minX, + maxX, maxY ); // apply any pending hover event @@ -130,6 +131,7 @@ export default class TxView implements TransactionStripped { temp: true }, minX, + maxX, maxY ); } @@ -137,6 +139,7 @@ export default class TxView implements TransactionStripped { this.sprite.update( toSpriteUpdate(params), minX, + maxX, maxY ); } diff --git a/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts b/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts index 214ea4e79..0b2c6d883 100644 --- a/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts +++ b/frontend/src/app/components/block-overview-multi/block-overview-multi.component.ts @@ -40,6 +40,7 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On @Input() isLoading: boolean; @Input() resolution: number; @Input() numBlocks: number; + @Input() padding: number = 0; @Input() blockWidth: number = 360; @Input() autofit: boolean = false; @Input() blockLimit: number; @@ -285,8 +286,8 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On for (const [index, pendingUpdate] of this.pendingUpdates.entries()) { if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) { this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction); + this.clearUpdateQueue(index); } - this.clearUpdateQueue(index); } } @@ -391,8 +392,8 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On this.gl.viewport(0, 0, this.displayWidth, this.displayHeight); } for (let i = 0; i < this.scenes.length; i++) { - const blocksPerRow = Math.floor(this.displayWidth / this.blockWidth); - const x = (i % blocksPerRow) * this.blockWidth; + const blocksPerRow = Math.floor((this.displayWidth + this.padding) / (this.blockWidth + this.padding)); + const x = (i % blocksPerRow) * (this.blockWidth + this.padding); const row = Math.floor(i / blocksPerRow); const y = this.displayHeight - ((row + 1) * this.blockWidth); if (this.scenes[i]) { @@ -401,7 +402,7 @@ export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, On } else { this.scenes[i] = new BlockScene({ x, y, width: this.blockWidth, height: this.blockWidth, resolution: this.resolution, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService, - highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: 0, + highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, colorFunction: this.getColorFunction() }); this.start(); } diff --git a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts index 2680b4c13..df96ea315 100644 --- a/frontend/src/app/components/eight-blocks/eight-blocks.component.ts +++ b/frontend/src/app/components/eight-blocks/eight-blocks.component.ts @@ -114,7 +114,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy { this.wrapBlocks = params.wrap !== 'false'; this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0; this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000; - this.animationOffset = this.padding * 2; + this.animationOffset = 0; if (this.autofit) { this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2); diff --git a/frontend/src/app/components/eight-mempool/eight-mempool.component.html b/frontend/src/app/components/eight-mempool/eight-mempool.component.html new file mode 100644 index 000000000..982fad08c --- /dev/null +++ b/frontend/src/app/components/eight-mempool/eight-mempool.component.html @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/frontend/src/app/components/eight-mempool/eight-mempool.component.scss b/frontend/src/app/components/eight-mempool/eight-mempool.component.scss new file mode 100644 index 000000000..6d2c9931c --- /dev/null +++ b/frontend/src/app/components/eight-mempool/eight-mempool.component.scss @@ -0,0 +1,69 @@ +.blocks { + width: 100%; + height: 100%; + min-width: 100vw; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: flex-start; + align-content: flex-start; + + &.wrap { + flex-wrap: wrap; + } + + .block-wrapper { + flex-grow: 0; + flex-shrink: 0; + position: relative; + --block-width: 1080px; + + .info { + position: absolute; + left: 8%; + top: 8%; + right: 8%; + bottom: 8%; + height: 84%; + width: 84%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: calc(var(--block-width) * 0.03); + text-shadow: 0 0 calc(var(--block-width) * 0.05) black; + + h1 { + font-size: 6em; + line-height: 1; + margin-bottom: calc(var(--block-width) * 0.03); + } + h2 { + font-size: 1.8em; + line-height: 1; + margin-bottom: calc(var(--block-width) * 0.03); + } + + .hash { + font-family: monospace; + word-wrap: break-word; + font-size: 1.4em; + line-height: 1; + margin-bottom: calc(var(--block-width) * 0.03); + } + + .mined-by { + position: absolute; + bottom: 0; + margin: auto; + text-align: center; + } + } + } + + .block-container { + overflow: hidden; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/eight-mempool/eight-mempool.component.ts b/frontend/src/app/components/eight-mempool/eight-mempool.component.ts new file mode 100644 index 000000000..b311d4372 --- /dev/null +++ b/frontend/src/app/components/eight-mempool/eight-mempool.component.ts @@ -0,0 +1,201 @@ +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { catchError } from 'rxjs/operators'; +import { Subject, Subscription, of } from 'rxjs'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; +import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { detectWebGL } from '../../shared/graphs.utils'; +import { animate, style, transition, trigger } from '@angular/animations'; +import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe'; +import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component'; +import { CacheService } from '../../services/cache.service'; +import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface'; + +function bestFitResolution(min, max, n): number { + const target = (min + max) / 2; + let bestScore = Infinity; + let best = null; + for (let i = min; i <= max; i++) { + const remainder = (n % i); + if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) { + bestScore = remainder; + best = i; + } + } + return best; +} + +interface BlockInfo extends BlockExtended { + timeString: string; +} + +@Component({ + selector: 'app-eight-mempool', + templateUrl: './eight-mempool.component.html', + styleUrls: ['./eight-mempool.component.scss'], + animations: [ + trigger('infoChange', [ + transition(':enter', [ + style({ opacity: 0 }), + animate('1000ms', style({ opacity: 1 })), + ]), + transition(':leave', [ + animate('1000ms 500ms', style({ opacity: 0 })) + ]) + ]), + ], +}) +export class EightMempoolComponent implements OnInit, OnDestroy { + network = ''; + strippedTransactions: { [height: number]: TransactionStripped[] } = {}; + webGlEnabled = true; + hoverTx: string | null = null; + + tipSubscription: Subscription; + networkChangedSubscription: Subscription; + queryParamsSubscription: Subscription; + graphChangeSubscription: Subscription; + blockSub: Subscription; + + chainDirection: string = 'right'; + poolDirection: string = 'left'; + + lastBlockHeight: number = 0; + lastBlockHeightUpdate: number[] = []; + numBlocks: number = 8; + blockIndices: number[] = []; + autofit: boolean = false; + padding: number = 0; + wrapBlocks: boolean = false; + blockWidth: number = 360; + animationDuration: number = 2000; + animationOffset: number = 0; + stagger: number = 0; + testing: boolean = true; + testHeight: number = 800000; + testShiftTimeout: number; + + showInfo: boolean = true; + blockInfo: BlockInfo[] = []; + + wrapperStyle = { + '--block-width': '1080px', + width: '1080px', + maxWidth: '1080px', + padding: '', + }; + containerStyle = {}; + resolution: number = 86; + + @ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent; + + constructor( + private route: ActivatedRoute, + private router: Router, + public stateService: StateService, + private websocketService: WebsocketService, + private apiService: ApiService, + private cacheService: CacheService, + private bytesPipe: BytesPipe, + ) { + this.webGlEnabled = this.stateService.isBrowser && detectWebGL(); + } + + ngOnInit(): void { + this.websocketService.want(['blocks']); + this.network = this.stateService.network; + + this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => { + // process update + if (isMempoolDelta(update)) { + // delta + this.updateBlock(update); + } else { + const transactionsStripped = update.transactions; + const inOldBlock = {}; + const inNewBlock = {}; + const added: TransactionStripped[] = []; + const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = []; + const removed: string[] = []; + for (const tx of transactionsStripped) { + inNewBlock[tx.txid] = true; + } + for (const txid of Object.keys(this.blockGraph?.scenes[update.block]?.txs || {})) { + inOldBlock[txid] = true; + if (!inNewBlock[txid]) { + removed.push(txid); + } + } + for (const tx of transactionsStripped) { + if (!inOldBlock[tx.txid]) { + added.push(tx); + } else { + changed.push({ + txid: tx.txid, + rate: tx.rate, + flags: tx.flags, + acc: tx.acc + }); + } + } + this.updateBlock({ + block: update.block, + removed, + changed, + added + }); + } + }); + + this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { + this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8; + this.blockIndices = [...Array(this.numBlocks).keys()]; + this.lastBlockHeightUpdate = this.blockIndices.map(() => 0); + this.autofit = params.autofit !== 'false'; + this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540; + this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : this.blockWidth; + this.wrapBlocks = params.wrap !== 'false'; + this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0; + this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000; + this.animationOffset = 0; + + if (this.autofit) { + this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2); + } else { + this.resolution = 86; + } + + this.wrapperStyle = { + '--block-width': this.blockWidth + 'px', + width: this.blockWidth + 'px', + maxWidth: this.blockWidth + 'px', + padding: (this.padding || 0) +'px 0px', + }; + + this.websocketService.startTrackMempoolBlocks(this.blockIndices); + }); + + this.networkChangedSubscription = this.stateService.networkChanged$ + .subscribe((network) => this.network = network); + } + + ngOnDestroy(): void { + this.stateService.markBlock$.next({}); + this.tipSubscription.unsubscribe(); + this.networkChangedSubscription?.unsubscribe(); + this.queryParamsSubscription?.unsubscribe(); + } + + updateBlock(delta: MempoolBlockDelta): void { + const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]); + if (blockMined) { + this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); + } else { + this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection); + } + + this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight; + } +} diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 7552224f5..125c61973 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -33,6 +33,7 @@ export interface WebsocketResponse { 'track-scriptpubkeys'?: string[]; 'track-asset'?: string; 'track-mempool-block'?: number; + 'track-mempool-blocks'?: number[]; 'track-rbf'?: string; 'track-rbf-summary'?: boolean; 'track-accelerations'?: boolean; diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 39e9d1af3..5dc16095c 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -29,12 +29,14 @@ export class WebsocketService { private isTrackingTx = false; private trackingTxId: string; private isTrackingMempoolBlock = false; + private isTrackingMempoolBlocks = false; private isTrackingRbf: 'all' | 'fullRbf' | false = false; private isTrackingRbfSummary = false; private isTrackingAddress: string | false = false; private isTrackingAddresses: string[] | false = false; private isTrackingAccelerations: boolean = false; private trackingMempoolBlock: number; + private trackingMempoolBlocks: number[]; private stoppingTrackMempoolBlock: any | null = null; private latestGitCommit = ''; private onlineCheckTimeout: number; @@ -122,6 +124,9 @@ export class WebsocketService { if (this.isTrackingMempoolBlock) { this.startTrackMempoolBlock(this.trackingMempoolBlock, true); } + if (this.isTrackingMempoolBlocks) { + this.startTrackMempoolBlocks(this.trackingMempoolBlocks); + } if (this.isTrackingRbf) { this.startTrackRbf(this.isTrackingRbf); } @@ -218,6 +223,13 @@ export class WebsocketService { return false; } + startTrackMempoolBlocks(blocks: number[], force: boolean = false): boolean { + this.websocketSubject.next({ 'track-mempool-blocks': blocks }); + this.isTrackingMempoolBlocks = true; + this.trackingMempoolBlocks = blocks; + return true; + } + stopTrackMempoolBlock(): void { if (this.stoppingTrackMempoolBlock) { clearTimeout(this.stoppingTrackMempoolBlock); @@ -231,6 +243,11 @@ export class WebsocketService { }, 2000); } + stopTrackMempoolBlocks(): void { + this.websocketSubject.next({ 'track-mempool-blocks': [] }); + this.isTrackingMempoolBlocks = false; + } + startTrackRbf(mode: 'all' | 'fullRbf') { this.websocketSubject.next({ 'track-rbf': mode }); this.isTrackingRbf = mode; @@ -433,20 +450,25 @@ export class WebsocketService { } if (response['projected-block-transactions']) { - if (response['projected-block-transactions'].index == this.trackingMempoolBlock) { - if (response['projected-block-transactions'].blockTransactions) { - this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; + if (response['projected-block-transactions'].index != null) { + const update = response['projected-block-transactions']; + if (update.blockTransactions) { this.stateService.mempoolBlockUpdate$.next({ - block: this.trackingMempoolBlock, - transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), + block: update.index, + transactions: update.blockTransactions.map(uncompressTx), }); - } else if (response['projected-block-transactions'].delta) { - if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) { - this.stateService.mempoolSequence = 0; - this.startTrackMempoolBlock(this.trackingMempoolBlock, true); - } else { - this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta)); + } else if (update.delta) { + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta)); + } + } else if (response['projected-block-transactions'].length) { + for (const update of response['projected-block-transactions']) { + if (update.blockTransactions) { + this.stateService.mempoolBlockUpdate$.next({ + block: update.index, + transactions: update.blockTransactions.map(uncompressTx), + }); + } else if (update.delta) { + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta)); } } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2b3129239..b7c4e7573 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -106,6 +106,7 @@ import { AccelerationSparklesComponent } from '../components/acceleration/sparkl import { BlockViewComponent } from '../components/block-view/block-view.component'; import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; +import { EightMempoolComponent } from '../components/eight-mempool/eight-mempool.component'; import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; import { ClockchainComponent } from '../components/clockchain/clockchain.component'; @@ -156,6 +157,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BlockchainComponent, BlockViewComponent, EightBlocksComponent, + EightMempoolComponent, MempoolBlockViewComponent, MempoolBlocksComponent, BlockchainBlocksComponent, @@ -219,6 +221,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir BitcoinsatoshisPipe, BlockViewComponent, EightBlocksComponent, + EightMempoolComponent, MempoolBlockViewComponent, MempoolBlockOverviewComponent, ClockchainComponent,