Merge branch 'master' into simon/load-more-mempool-txs

This commit is contained in:
wiz 2023-07-14 19:30:15 +09:00 committed by GitHub
commit e3ddde9c90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 298 additions and 138 deletions

View File

@ -1,12 +1,12 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",

View File

@ -1,12 +1,12 @@
{ {
"name": "gbt", "name": "gbt",
"version": "0.1.0", "version": "3.0.0-dev",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gbt", "name": "gbt",
"version": "0.1.0", "version": "3.0.0-dev",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@napi-rs/cli": "^2.16.1" "@napi-rs/cli": "^2.16.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "gbt", "name": "gbt",
"version": "0.1.0", "version": "3.0.0-dev",
"description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust", "description": "An inefficient re-implementation of the getBlockTemplate algorithm in Rust",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
@ -30,4 +30,4 @@
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
} }
} }

View File

@ -6,6 +6,7 @@ import { Common } from "./common";
interface RbfTransaction extends TransactionStripped { interface RbfTransaction extends TransactionStripped {
rbf?: boolean; rbf?: boolean;
mined?: boolean; mined?: boolean;
fullRbf?: boolean;
} }
interface RbfTree { interface RbfTree {
@ -17,6 +18,16 @@ interface RbfTree {
replaces: RbfTree[]; replaces: RbfTree[];
} }
export interface ReplacementInfo {
mined: boolean;
fullRbf: boolean;
txid: string;
oldFee: number;
oldVsize: number;
newFee: number;
newVsize: number;
}
class RbfCache { class RbfCache {
private replacedBy: Map<string, string> = new Map(); private replacedBy: Map<string, string> = new Map();
private replaces: Map<string, string[]> = new Map(); private replaces: Map<string, string[]> = new Map();
@ -41,11 +52,15 @@ class RbfCache {
this.txs.set(newTx.txid, newTxExtended); this.txs.set(newTx.txid, newTxExtended);
// maintain rbf trees // maintain rbf trees
let fullRbf = false; let txFullRbf = false;
let treeFullRbf = false;
const replacedTrees: RbfTree[] = []; const replacedTrees: RbfTree[] = [];
for (const replacedTxExtended of replaced) { for (const replacedTxExtended of replaced) {
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction; const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe); replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
if (!replacedTx.rbf) {
txFullRbf = true;
}
this.replacedBy.set(replacedTx.txid, newTx.txid); this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) { if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid); const treeId = this.treeMap.get(replacedTx.txid);
@ -55,7 +70,7 @@ class RbfCache {
if (tree) { if (tree) {
tree.interval = newTime - tree?.time; tree.interval = newTime - tree?.time;
replacedTrees.push(tree); replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf; treeFullRbf = treeFullRbf || tree.fullRbf || !tree.tx.rbf;
} }
} }
} else { } else {
@ -67,15 +82,16 @@ class RbfCache {
fullRbf: !replacedTx.rbf, fullRbf: !replacedTx.rbf,
replaces: [], replaces: [],
}); });
fullRbf = fullRbf || !replacedTx.rbf; treeFullRbf = treeFullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended); this.txs.set(replacedTx.txid, replacedTxExtended);
} }
} }
newTx.fullRbf = txFullRbf;
const treeId = replacedTrees[0].tx.txid; const treeId = replacedTrees[0].tx.txid;
const newTree = { const newTree = {
tx: newTx, tx: newTx,
time: newTime, time: newTime,
fullRbf, fullRbf: treeFullRbf,
replaces: replacedTrees replaces: replacedTrees
}; };
this.rbfTrees.set(treeId, newTree); this.rbfTrees.set(treeId, newTree);
@ -349,6 +365,27 @@ class RbfCache {
} }
return tree; return tree;
} }
public getLatestRbfSummary(): ReplacementInfo[] {
const rbfList = this.getRbfTrees(false);
return rbfList.slice(0, 6).map(rbfTree => {
let oldFee = 0;
let oldVsize = 0;
for (const replaced of rbfTree.replaces) {
oldFee += replaced.tx.fee;
oldVsize += replaced.tx.vsize;
}
return {
txid: rbfTree.tx.txid,
mined: !!rbfTree.tx.mined,
fullRbf: !!rbfTree.tx.fullRbf,
oldFee,
oldVsize,
newFee: rbfTree.tx.fee,
newVsize: rbfTree.tx.vsize,
};
});
}
} }
export default new RbfCache(); export default new RbfCache();

View File

@ -12,7 +12,7 @@ import { Common } from './common';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import config from '../config'; import config from '../config';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache'; import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
@ -40,6 +40,7 @@ class WebsocketHandler {
private socketData: { [key: string]: string } = {}; private socketData: { [key: string]: string } = {};
private serializedInitData: string = '{}'; private serializedInitData: string = '{}';
private lastRbfSummary: ReplacementInfo | null = null;
constructor() { } constructor() { }
@ -225,6 +226,15 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-rbf-summary'] != null) {
if (parsedMessage['track-rbf-summary']) {
client['track-rbf-summary'] = true;
response['rbfLatestSummary'] = this.socketData['rbfSummary'];
} else {
client['track-rbf-summary'] = false;
}
}
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
if (!this.socketData['blocks']?.length || !this.socketData['da']) { if (!this.socketData['blocks']?.length || !this.socketData['da']) {
this.updateSocketData(); this.updateSocketData();
@ -395,10 +405,13 @@ class WebsocketHandler {
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
let fullRbfReplacements; let fullRbfReplacements;
let rbfSummary;
if (Object.keys(rbfChanges.trees).length) { if (Object.keys(rbfChanges.trees).length) {
rbfReplacements = rbfCache.getRbfTrees(false); rbfReplacements = rbfCache.getRbfTrees(false);
fullRbfReplacements = rbfCache.getRbfTrees(true); fullRbfReplacements = rbfCache.getRbfTrees(true);
rbfSummary = rbfCache.getLatestRbfSummary();
} }
for (const deletedTx of deletedTransactions) { for (const deletedTx of deletedTransactions) {
rbfCache.evict(deletedTx.txid); rbfCache.evict(deletedTx.txid);
} }
@ -409,7 +422,7 @@ class WebsocketHandler {
const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); const latestTransactions = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
// update init data // update init data
this.updateSocketDataFields({ const socketDataFields = {
'mempoolInfo': mempoolInfo, 'mempoolInfo': mempoolInfo,
'vBytesPerSecond': vBytesPerSecond, 'vBytesPerSecond': vBytesPerSecond,
'mempool-blocks': mBlocks, 'mempool-blocks': mBlocks,
@ -417,7 +430,11 @@ class WebsocketHandler {
'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined, 'da': da?.previousTime ? da : undefined,
'fees': recommendedFees, 'fees': recommendedFees,
}); };
if (rbfSummary) {
socketDataFields['rbfSummary'] = rbfSummary;
}
this.updateSocketDataFields(socketDataFields);
// cache serialized objects to avoid stringify-ing the same thing for every client // cache serialized objects to avoid stringify-ing the same thing for every client
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
@ -601,6 +618,10 @@ class WebsocketHandler {
response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements); response['rbfLatest'] = getCachedResponse('fullrbfLatest', fullRbfReplacements);
} }
if (client['track-rbf-summary'] && rbfSummary) {
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
const serializedResponse = this.serializeResponse(response); const serializedResponse = this.serializeResponse(response);
client.send(serializedResponse); client.send(serializedResponse);

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 28, 2023.
Signed: bennyhodl

View File

@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^14.2.10", "@angular-devkit/build-angular": "^14.2.10",

View File

@ -1,6 +1,6 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "3.0.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@ -119,4 +119,4 @@
"scarfSettings": { "scarfSettings": {
"enabled": false "enabled": false
} }
} }

View File

@ -1,15 +1,17 @@
<div class="block-overview-graph">
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
<app-block-overview-tooltip <div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
[tx]="selectedTx || hoverTx" <div class="block-overview-graph">
[cursorPosition]="tooltipPosition" <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
[clickable]="!!selectedTx" <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
[auditEnabled]="auditHighlighting" <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
[blockConversion]="blockConversion" <div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
></app-block-overview-tooltip> </div>
<app-block-overview-tooltip
[tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition"
[clickable]="!!selectedTx"
[auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion"
></app-block-overview-tooltip>
</div>
</div> </div>

View File

@ -6,8 +6,16 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
grid-column: 1/-1;
} }
.grid-align {
position: relative;
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, 75px);
justify-content: center;
}
.block-overview-canvas { .block-overview-canvas {
position: absolute; position: absolute;

View File

@ -6,6 +6,8 @@ import TxSprite from './tx-sprite';
import TxView from './tx-view'; import TxView from './tx-view';
import { Position } from './sprite-types'; import { Position } from './sprite-types';
import { Price } from '../../services/price.service'; import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-block-overview-graph', selector: 'app-block-overview-graph',
@ -23,7 +25,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false; @Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false; @Input() auditHighlighting: boolean = false;
@Input() blockConversion: Price; @Input() blockConversion: Price;
@Input() pixelAlign: boolean = false;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter<string>(); @Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter();
@ -44,16 +45,25 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
scene: BlockScene; scene: BlockScene;
hoverTx: TxView | void; hoverTx: TxView | void;
selectedTx: TxView | void; selectedTx: TxView | void;
highlightTx: TxView | void;
mirrorTx: TxView | void; mirrorTx: TxView | void;
tooltipPosition: Position; tooltipPosition: Position;
readyNextFrame = false; readyNextFrame = false;
searchText: string;
searchSubscription: Subscription;
constructor( constructor(
readonly ngZone: NgZone, readonly ngZone: NgZone,
readonly elRef: ElementRef, readonly elRef: ElementRef,
private stateService: StateService,
) { ) {
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize); this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
this.updateSearchHighlight();
});
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -109,6 +119,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.scene.setup(transactions); this.scene.setup(transactions);
this.readyNextFrame = true; this.readyNextFrame = true;
this.start(); this.start();
this.updateSearchHighlight();
} }
} }
@ -116,6 +127,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) { if (this.scene) {
this.scene.enter(transactions, direction); this.scene.enter(transactions, direction);
this.start(); this.start();
this.updateSearchHighlight();
} }
} }
@ -123,6 +135,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) { if (this.scene) {
this.scene.exit(direction); this.scene.exit(direction);
this.start(); this.start();
this.updateSearchHighlight();
} }
} }
@ -130,6 +143,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) { if (this.scene) {
this.scene.replace(transactions || [], direction, sort); this.scene.replace(transactions || [], direction, sort);
this.start(); this.start();
this.updateSearchHighlight();
} }
} }
@ -137,6 +151,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) { if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, change, direction, resetLayout);
this.start(); this.start();
this.updateSearchHighlight();
} }
} }
@ -203,7 +218,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else { } else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign }); highlighting: this.auditHighlighting });
this.start(); this.start();
} }
} }
@ -406,6 +421,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
updateSearchHighlight(): void {
if (this.highlightTx && this.highlightTx.txid !== this.searchText && this.scene) {
this.scene.setHighlight(this.highlightTx, false);
this.start();
} else if (this.scene?.txs && this.searchText && this.searchText.length === 64) {
this.highlightTx = this.scene.txs[this.searchText];
if (this.highlightTx) {
this.scene.setHighlight(this.highlightTx, true);
this.start();
}
}
}
setHighlightingEnabled(enabled: boolean): void { setHighlightingEnabled(enabled: boolean): void {
if (this.scene) { if (this.scene) {
this.scene.setHighlighting(enabled); this.scene.setHighlighting(enabled);

View File

@ -15,7 +15,6 @@ export default class BlockScene {
gridWidth: number; gridWidth: number;
gridHeight: number; gridHeight: number;
gridSize: number; gridSize: number;
pixelAlign: boolean;
vbytesPerUnit: number; vbytesPerUnit: number;
unitPadding: number; unitPadding: number;
unitWidth: number; unitWidth: number;
@ -24,24 +23,19 @@ export default class BlockScene {
animateUntil = 0; animateUntil = 0;
dirty: boolean; dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) { ) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }); this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
} }
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.gridSize = this.width / this.gridWidth; this.gridSize = this.width / this.gridWidth;
if (this.pixelAlign) { this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.unitWidth = this.gridSize - (this.unitPadding);
} else {
this.unitPadding = width / 500;
this.unitWidth = this.gridSize - (this.unitPadding * 2);
}
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
@ -215,15 +209,18 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value)); this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
} }
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: setHighlight(tx: TxView, value: boolean): void {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void { ): void {
this.orientation = orientation; this.orientation = orientation;
this.flip = flip; this.flip = flip;
this.vertexArray = vertexArray; this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting; this.highlightingEnabled = highlighting;
this.pixelAlign = pixelAlign;
this.scene = { this.scene = {
count: 0, count: 0,
@ -349,12 +346,7 @@ export default class BlockScene {
private gridToScreen(position: Square | void): Square { private gridToScreen(position: Square | void): Square {
if (position) { if (position) {
const slotSize = (position.s * this.gridSize); const slotSize = (position.s * this.gridSize);
let squareSize; const squareSize = slotSize - (this.unitPadding * 2);
if (this.pixelAlign) {
squareSize = slotSize - (this.unitPadding);
} else {
squareSize = slotSize - (this.unitPadding * 2);
}
// The grid is laid out notionally left-to-right, bottom-to-top, // The grid is laid out notionally left-to-right, bottom-to-top,
// so we rotate and/or flip the y axis to match the target configuration. // so we rotate and/or flip the y axis to match the target configuration.
@ -430,7 +422,7 @@ export default class BlockScene {
// calculates and returns the size of the tx in multiples of the grid size // calculates and returns the size of the tx in multiples of the grid size
private txSize(tx: TxView): number { private txSize(tx: TxView): number {
const scale = Math.max(1, Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit))); const scale = Math.max(1, Math.round(Math.sqrt(1.1 * tx.vsize / this.vbytesPerUnit)));
return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!) return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
} }

View File

@ -7,6 +7,7 @@ import BlockScene from './block-scene';
const hoverTransitionTime = 300; const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4'); const defaultHoverColor = hexToColor('1bd8f4');
const defaultHighlightColor = hexToColor('800080');
const feeColors = mempoolFeeColors.map(hexToColor); const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
@ -44,8 +45,10 @@ export default class TxView implements TransactionStripped {
initialised: boolean; initialised: boolean;
vertexArray: FastVertexArray; vertexArray: FastVertexArray;
hover: boolean; hover: boolean;
highlight: boolean;
sprite: TxSprite; sprite: TxSprite;
hoverColor: Color | void; hoverColor: Color | void;
highlightColor: Color | void;
screenPosition: Square; screenPosition: Square;
gridPosition: Square | void; gridPosition: Square | void;
@ -150,8 +153,40 @@ export default class TxView implements TransactionStripped {
} else { } else {
this.hover = false; this.hover = false;
this.hoverColor = null; this.hoverColor = null;
if (this.sprite) { if (this.highlight) {
this.sprite.resume(hoverTransitionTime); this.setHighlight(true, this.highlightColor);
} else {
if (this.sprite) {
this.sprite.resume(hoverTransitionTime);
}
}
}
this.dirty = false;
return performance.now() + hoverTransitionTime;
}
// Temporarily override the tx color
// returns minimum transition end time
setHighlight(highlightOn: boolean, color: Color | void = defaultHighlightColor): number {
if (highlightOn) {
this.highlight = true;
this.highlightColor = color;
this.sprite.update({
...this.highlightColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
} else {
this.highlight = false;
this.highlightColor = null;
if (this.hover) {
this.setHover(true, this.hoverColor);
} else {
if (this.sprite) {
this.sprite.resume(hoverTransitionTime);
}
} }
} }
this.dirty = false; this.dirty = false;

View File

@ -71,7 +71,7 @@
<app-block-overview-graph <app-block-overview-graph
#blockGraph #blockGraph
[isLoading]="false" [isLoading]="false"
[resolution]="75" [resolution]="80"
[blockLimit]="stateService.blockVSize" [blockLimit]="stateService.blockVSize"
[orientation]="'top'" [orientation]="'top'"
[flip]="false" [flip]="false"

View File

@ -52,8 +52,8 @@
.chart-container { .chart-container {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
width: 470px; width: 480px;
min-width: 470px; min-width: 480px;
padding: 0; padding: 0;
margin-right: 15px; margin-right: 15px;
} }

View File

@ -100,7 +100,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="col-sm"> <div class="col-sm" [class.graph-col]="webGlEnabled && !showAudit">
<table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)"> <table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
<tbody> <tbody>
<ng-container *ngTemplateOutlet="restOfTable"></ng-container> <ng-container *ngTemplateOutlet="restOfTable"></ng-container>
@ -110,7 +110,7 @@
<app-block-overview-graph <app-block-overview-graph
#blockGraphActual #blockGraphActual
[isLoading]="isLoadingOverview" [isLoading]="isLoadingOverview"
[resolution]="75" [resolution]="86"
[blockLimit]="stateService.blockVSize" [blockLimit]="stateService.blockVSize"
[orientation]="'top'" [orientation]="'top'"
[flip]="false" [flip]="false"
@ -227,7 +227,7 @@
<div class="col-sm"> <div class="col-sm">
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3> <h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
<div class="block-graph-wrapper"> <div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75" <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph> (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
@ -239,7 +239,7 @@
<div class="col-sm" *ngIf="!isMobile"> <div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3> <h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
<div class="block-graph-wrapper"> <div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75" <app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph> (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>

View File

@ -239,6 +239,7 @@ h1 {
.nav-tabs { .nav-tabs {
border-color: white; border-color: white;
border-width: 1px; border-width: 1px;
margin-bottom: 1em;
} }
.nav-tabs .nav-link { .nav-tabs .nav-link {
@ -293,3 +294,7 @@ h1 {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
} }
.graph-col {
flex-grow: 1.11;
}

View File

@ -25,7 +25,7 @@
</ng-container> </ng-container>
<ng-template #mempoolMode> <ng-template #mempoolMode>
<div class="block-sizer" [style]="blockSizerStyle"> <div class="block-sizer" [style]="blockSizerStyle">
<app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview> <app-mempool-block-overview [index]="blockIndex"></app-mempool-block-overview>
</div> </div>
</ng-template> </ng-template>
<div class="fader"></div> <div class="fader"></div>

View File

@ -130,6 +130,7 @@ export class HashrateChartComponent implements OnInit {
}); });
++hashIndex; ++hashIndex;
} }
diffIndex++;
break; break;
} }

View File

@ -1,10 +1,9 @@
<app-block-overview-graph <app-block-overview-graph
#blockGraph #blockGraph
[isLoading]="isLoading$ | async" [isLoading]="isLoading$ | async"
[resolution]="75" [resolution]="86"
[blockLimit]="stateService.blockVSize" [blockLimit]="stateService.blockVSize"
[orientation]="timeLtr ? 'right' : 'left'" [orientation]="timeLtr ? 'right' : 'left'"
[flip]="true" [flip]="true"
[pixelAlign]="pixelAlign"
(txClickEvent)="onTxClick($event)" (txClickEvent)="onTxClick($event)"
></app-block-overview-graph> ></app-block-overview-graph>

View File

@ -16,7 +16,6 @@ import { Router } from '@angular/router';
}) })
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number; @Input() index: number;
@Input() pixelAlign: boolean = false;
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;

View File

@ -80,6 +80,9 @@ export class SearchFormComponent implements OnInit {
} }
return text.trim(); return text.trim();
}), }),
tap((text) => {
this.stateService.searchText$.next(text);
}),
distinctUntilChanged(), distinctUntilChanged(),
); );

View File

@ -75,36 +75,31 @@
<div class="col" style="max-height: 410px"> <div class="col" style="max-height: 410px">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5> <h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
</a> </a>
<table class="table lastest-blocks-table"> <table class="table lastest-replacements-table">
<thead> <thead>
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th> <th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th> <th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
<th *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4" i18n="mining.pool-name">Pool</th> <th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th> <th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock"> <tr *ngFor="let replacement of replacements$ | async;">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td class="table-cell-txid">
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td> <a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4"> <app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = '/resources/mining-pools/default.svg'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a> </a>
</td> </td>
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td> <td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
<td class="table-cell-size"> <td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
<div class="progress"> <td class="table-cell-badges">
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }">&nbsp;</div> <span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div> <span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
</div> <span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="transaction.rbf">RBF</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -175,39 +175,43 @@
height: 18px; height: 18px;
} }
.lastest-blocks-table { .lastest-replacements-table {
width: 100%; width: 100%;
text-align: left; text-align: left;
table-layout:fixed;
tr, td, th { tr, td, th {
border: 0px; border: 0px;
padding-top: 0.65rem !important; padding-top: 0.71rem !important;
padding-bottom: 0.7rem !important; padding-bottom: 0.75rem !important;
} }
.table-cell-height { td {
width: 15%; overflow:hidden;
width: 25%;
} }
.table-cell-mined { .table-cell-txid {
width: 35%; width: 25%;
text-align: left; text-align: start;
} }
.table-cell-transaction-count { .table-cell-old-fee {
display: none; width: 25%;
text-align: right; text-align: end;
width: 20%;
display: table-cell; @media(max-width: 1080px) {
}
.table-cell-size {
display: none;
text-align: center;
width: 30%;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none; display: none;
} }
@media (min-width: 992px) { }
display: table-cell; .table-cell-new-fee {
width: 20%;
text-align: end;
}
.table-cell-badges {
width: 23%;
padding-right: 0;
padding-left: 5px;
text-align: end;
.badge {
margin-left: 5px;
} }
} }
} }

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; import { filter, map, scan, share, switchMap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service'; import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
@ -38,8 +38,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
mempoolInfoData$: Observable<MempoolInfoData>; mempoolInfoData$: Observable<MempoolInfoData>;
mempoolLoadingStatus$: Observable<number>; mempoolLoadingStatus$: Observable<number>;
vBytesPerSecondLimit = 1667; vBytesPerSecondLimit = 1667;
blocks$: Observable<BlockExtended[]>;
transactions$: Observable<TransactionStripped[]>; transactions$: Observable<TransactionStripped[]>;
replacements$: Observable<ReplacementInfo[]>;
latestBlockHeight: number; latestBlockHeight: number;
mempoolTransactionsWeightPerSecondData: any; mempoolTransactionsWeightPerSecondData: any;
mempoolStats$: Observable<MempoolStatsData>; mempoolStats$: Observable<MempoolStatsData>;
@ -58,12 +58,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.currencySubscription.unsubscribe(); this.currencySubscription.unsubscribe();
this.websocketService.stopTrackRbfSummary();
} }
ngOnInit(): void { ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle(); this.seoService.resetTitle();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']); this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.websocketService.startTrackRbfSummary();
this.network$ = merge(of(''), this.stateService.networkChanged$); this.network$ = merge(of(''), this.stateService.networkChanged$);
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$ this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe( .pipe(
@ -130,23 +132,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
}), }),
); );
this.blocks$ = this.stateService.blocks$
.pipe(
tap((blocks) => {
this.latestBlockHeight = blocks[0].height;
}),
switchMap((blocks) => {
if (this.stateService.env.MINING_DASHBOARD === true) {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
}
return of(blocks.slice(0, 6));
})
);
this.transactions$ = this.stateService.transactions$ this.transactions$ = this.stateService.transactions$
.pipe( .pipe(
scan((acc, tx) => { scan((acc, tx) => {
@ -159,6 +144,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
}, []), }, []),
); );
this.replacements$ = this.stateService.rbfLatestSummary$;
this.mempoolStats$ = this.stateService.connectionState$ this.mempoolStats$ = this.stateService.connectionState$
.pipe( .pipe(
filter((state) => state === 2), filter((state) => state === 2),
@ -219,4 +206,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
trackByBlock(index: number, block: BlockExtended) { trackByBlock(index: number, block: BlockExtended) {
return block.height; return block.height;
} }
checkFullRbf(tree: RbfTree): void {
let fullRbf = false;
for (const replaced of tree.replaces) {
if (!replaced.tx.rbf) {
fullRbf = true;
}
replaced.replacedBy = tree.tx;
this.checkFullRbf(replaced);
}
tree.tx.fullRbf = fullRbf;
}
} }

View File

@ -18,6 +18,7 @@ export interface WebsocketResponse {
txReplaced?: ReplacedTransaction; txReplaced?: ReplacedTransaction;
rbfInfo?: RbfTree; rbfInfo?: RbfTree;
rbfLatest?: RbfTree[]; rbfLatest?: RbfTree[];
rbfLatestSummary?: ReplacementInfo[];
utxoSpent?: object; utxoSpent?: object;
transactions?: TransactionStripped[]; transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators; loadingIndicators?: ILoadingIndicators;
@ -29,6 +30,7 @@ export interface WebsocketResponse {
'track-asset'?: string; 'track-asset'?: string;
'track-mempool-block'?: number; 'track-mempool-block'?: number;
'track-rbf'?: string; 'track-rbf'?: string;
'track-rbf-summary'?: boolean;
'watch-mempool'?: boolean; 'watch-mempool'?: boolean;
'track-bisq-market'?: string; 'track-bisq-market'?: string;
'refresh-blocks'?: boolean; 'refresh-blocks'?: boolean;
@ -37,6 +39,16 @@ export interface WebsocketResponse {
export interface ReplacedTransaction extends Transaction { export interface ReplacedTransaction extends Transaction {
txid: string; txid: string;
} }
export interface ReplacementInfo {
mined: boolean;
fullRbf: boolean;
txid: string;
oldFee: number;
oldVsize: number;
newFee: number;
newVsize: number;
}
export interface MempoolBlock { export interface MempoolBlock {
blink?: boolean; blink?: boolean;
height?: number; height?: number;

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -108,6 +108,7 @@ export class StateService {
txReplaced$ = new Subject<ReplacedTransaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>(); txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>(); rbfLatest$ = new Subject<RbfTree[]>();
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
utxoSpent$ = new Subject<object>(); utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1); difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>(); mempoolTransactions$ = new Subject<Transaction>();
@ -129,6 +130,7 @@ export class StateService {
markBlock$ = new BehaviorSubject<MarkBlockState>({}); markBlock$ = new BehaviorSubject<MarkBlockState>({});
keyNavigation$ = new Subject<KeyboardEvent>(); keyNavigation$ = new Subject<KeyboardEvent>();
searchText$ = new BehaviorSubject<string>('');
blockScrolling$: Subject<boolean> = new Subject<boolean>(); blockScrolling$: Subject<boolean> = new Subject<boolean>();
resetScroll$: Subject<boolean> = new Subject<boolean>(); resetScroll$: Subject<boolean> = new Subject<boolean>();

View File

@ -29,6 +29,7 @@ export class WebsocketService {
private trackingTxId: string; private trackingTxId: string;
private isTrackingMempoolBlock = false; private isTrackingMempoolBlock = false;
private isTrackingRbf = false; private isTrackingRbf = false;
private isTrackingRbfSummary = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -185,6 +186,16 @@ export class WebsocketService {
this.isTrackingRbf = false; this.isTrackingRbf = false;
} }
startTrackRbfSummary() {
this.websocketSubject.next({ 'track-rbf-summary': true });
this.isTrackingRbfSummary = true;
}
stopTrackRbfSummary() {
this.websocketSubject.next({ 'track-rbf-summary': false });
this.isTrackingRbfSummary = false;
}
startTrackBisqMarket(market: string) { startTrackBisqMarket(market: string) {
this.websocketSubject.next({ 'track-bisq-market': market }); this.websocketSubject.next({ 'track-bisq-market': market });
} }
@ -283,6 +294,10 @@ export class WebsocketService {
this.stateService.rbfLatest$.next(response.rbfLatest); this.stateService.rbfLatest$.next(response.rbfLatest);
} }
if (response.rbfLatestSummary) {
this.stateService.rbfLatestSummary$.next(response.rbfLatestSummary);
}
if (response.txReplaced) { if (response.txReplaced) {
this.stateService.txReplaced$.next(response.txReplaced); this.stateService.txReplaced$.next(response.txReplaced);
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "mempool-unfurl", "name": "mempool-unfurl",
"version": "0.1.0", "version": "3.0.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-unfurl", "name": "mempool-unfurl",
"version": "0.1.0", "version": "3.0.0-dev",
"dependencies": { "dependencies": {
"@types/node": "^16.11.41", "@types/node": "^16.11.41",
"express": "^4.18.0", "express": "^4.18.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "mempool-unfurl", "name": "mempool-unfurl",
"version": "0.1.0", "version": "3.0.0-dev",
"description": "Renderer for mempool open graph link preview images", "description": "Renderer for mempool open graph link preview images",
"repository": { "repository": {
"type": "git", "type": "git",