Merge pull request #4858 from mempool/mononaut/better-audits
Betterer audits
This commit is contained in:
commit
f833904d7b
@ -7,13 +7,14 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
|
|||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
|
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||||
@ -68,20 +69,27 @@ class Audit {
|
|||||||
|
|
||||||
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||||
// these displaced transactions should occupy the first N weight units of the next projected block
|
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||||
let displacedWeightRemaining = displacedWeight;
|
let displacedWeightRemaining = displacedWeight + 4000;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let lastFeeRate = Infinity;
|
let lastFeeRate = Infinity;
|
||||||
let failures = 0;
|
let failures = 0;
|
||||||
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
|
let blockIndex = 1;
|
||||||
const txid = projectedBlocks[1].transactionIds[index];
|
while (projectedBlocks[blockIndex] && failures < 500) {
|
||||||
|
if (index >= projectedBlocks[blockIndex].transactionIds.length) {
|
||||||
|
index = 0;
|
||||||
|
blockIndex++;
|
||||||
|
}
|
||||||
|
const txid = projectedBlocks[blockIndex].transactionIds[index];
|
||||||
const tx = mempool[txid];
|
const tx = mempool[txid];
|
||||||
if (tx) {
|
if (tx) {
|
||||||
const fits = (tx.weight - displacedWeightRemaining) < 4000;
|
const fits = (tx.weight - displacedWeightRemaining) < 4000;
|
||||||
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate;
|
// 0.005 margin of error for any remaining vsize rounding issues
|
||||||
|
const feeMatches = tx.effectiveFeePerVsize >= (lastFeeRate - 0.005);
|
||||||
if (fits || feeMatches) {
|
if (fits || feeMatches) {
|
||||||
isDisplaced[txid] = true;
|
isDisplaced[txid] = true;
|
||||||
if (fits) {
|
if (fits) {
|
||||||
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
|
// (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize) attempts to correct for vsize rounding in the simple non-CPFP case
|
||||||
|
lastFeeRate = Math.min(lastFeeRate, (tx.effectiveFeePerVsize * tx.vsize) / Math.ceil(tx.vsize));
|
||||||
}
|
}
|
||||||
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
|
||||||
displacedWeightRemaining -= tx.weight;
|
displacedWeightRemaining -= tx.weight;
|
||||||
@ -106,7 +114,11 @@ class Audit {
|
|||||||
if (rbfCache.has(tx.txid)) {
|
if (rbfCache.has(tx.txid)) {
|
||||||
rbf.push(tx.txid);
|
rbf.push(tx.txid);
|
||||||
} else if (!isDisplaced[tx.txid]) {
|
} else if (!isDisplaced[tx.txid]) {
|
||||||
added.push(tx.txid);
|
if (mempool[tx.txid]) {
|
||||||
|
prioritized.push(tx.txid);
|
||||||
|
} else {
|
||||||
|
added.push(tx.txid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
overflowWeight += tx.weight;
|
overflowWeight += tx.weight;
|
||||||
}
|
}
|
||||||
@ -155,6 +167,7 @@ class Audit {
|
|||||||
return {
|
return {
|
||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
|
prioritized,
|
||||||
fresh,
|
fresh,
|
||||||
sigop: [],
|
sigop: [],
|
||||||
fullrbf: rbf,
|
fullrbf: rbf,
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 75;
|
private static currentVersion = 76;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -654,6 +654,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||||
await this.updateToSchemaVersion(75);
|
await this.updateToSchemaVersion(75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 76 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(76);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -869,7 +869,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||||
@ -895,6 +895,7 @@ class WebsocketHandler {
|
|||||||
height: block.height,
|
height: block.height,
|
||||||
hash: block.id,
|
hash: block.id,
|
||||||
addedTxs: added,
|
addedTxs: added,
|
||||||
|
prioritizedTxs: prioritized,
|
||||||
missingTxs: censored,
|
missingTxs: censored,
|
||||||
freshTxs: fresh,
|
freshTxs: fresh,
|
||||||
sigopTxs: sigop,
|
sigopTxs: sigop,
|
||||||
|
@ -37,6 +37,7 @@ export interface BlockAudit {
|
|||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
fullrbfTxs: string[],
|
fullrbfTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
|
prioritizedTxs: string[],
|
||||||
acceleratedTxs: string[],
|
acceleratedTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees?: number,
|
expectedFees?: number,
|
||||||
|
@ -114,6 +114,7 @@ class AuditReplication {
|
|||||||
time: auditSummary.timestamp || auditSummary.time,
|
time: auditSummary.timestamp || auditSummary.time,
|
||||||
missingTxs: auditSummary.missingTxs || [],
|
missingTxs: auditSummary.missingTxs || [],
|
||||||
addedTxs: auditSummary.addedTxs || [],
|
addedTxs: auditSummary.addedTxs || [],
|
||||||
|
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||||
freshTxs: auditSummary.freshTxs || [],
|
freshTxs: auditSummary.freshTxs || [],
|
||||||
sigopTxs: auditSummary.sigopTxs || [],
|
sigopTxs: auditSummary.sigopTxs || [],
|
||||||
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
||||||
|
@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
|||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||||
@ -66,6 +66,7 @@ class BlocksAuditRepositories {
|
|||||||
template,
|
template,
|
||||||
missing_txs as missingTxs,
|
missing_txs as missingTxs,
|
||||||
added_txs as addedTxs,
|
added_txs as addedTxs,
|
||||||
|
prioritized_txs as prioritizedTxs,
|
||||||
fresh_txs as freshTxs,
|
fresh_txs as freshTxs,
|
||||||
sigop_txs as sigopTxs,
|
sigop_txs as sigopTxs,
|
||||||
fullrbf_txs as fullrbfTxs,
|
fullrbf_txs as fullrbfTxs,
|
||||||
@ -81,6 +82,7 @@ class BlocksAuditRepositories {
|
|||||||
if (rows.length) {
|
if (rows.length) {
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
|
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||||
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
|
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from './fast-vertex-array';
|
||||||
import BlockScene from './block-scene';
|
import BlockScene from './block-scene';
|
||||||
import TxSprite from './tx-sprite';
|
import TxSprite from './tx-sprite';
|
||||||
@ -20,7 +20,7 @@ const unmatchedAuditColors = {
|
|||||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||||
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
||||||
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity),
|
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
|
||||||
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FastVertexArray } from './fast-vertex-array';
|
import { FastVertexArray } from './fast-vertex-array';
|
||||||
import TxView from './tx-view';
|
import TxView from './tx-view';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
||||||
import { defaultColorFunction } from './utils';
|
import { defaultColorFunction } from './utils';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
flags: number;
|
flags: number;
|
||||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||||
time?: number;
|
time?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
scene?: BlockScene;
|
scene?: BlockScene;
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export const defaultAuditColors = {
|
|||||||
censored: hexToColor('f344df'),
|
censored: hexToColor('f344df'),
|
||||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||||
added: hexToColor('0099ff'),
|
added: hexToColor('0099ff'),
|
||||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||||
accelerated: hexToColor('8F5FF6'),
|
accelerated: hexToColor('8F5FF6'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,6 +81,8 @@ export function defaultColorFunction(
|
|||||||
return auditColors.missing;
|
return auditColors.missing;
|
||||||
case 'added':
|
case 'added':
|
||||||
return auditColors.added;
|
return auditColors.added;
|
||||||
|
case 'prioritized':
|
||||||
|
return auditColors.prioritized;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'accelerated':
|
case 'accelerated':
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
<span *ngSwitchCase="'fresh'" class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span>
|
<span *ngSwitchCase="'fresh'" class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span>
|
||||||
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
|
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
|
||||||
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="transaction.audit.added">Added</span>
|
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="transaction.audit.added">Added</span>
|
||||||
|
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="transaction.audit.prioritized">Prioritized</span>
|
||||||
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
||||||
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span>
|
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span>
|
||||||
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||||
|
@ -371,6 +371,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
const inTemplate = {};
|
const inTemplate = {};
|
||||||
const inBlock = {};
|
const inBlock = {};
|
||||||
const isAdded = {};
|
const isAdded = {};
|
||||||
|
const isPrioritized = {};
|
||||||
const isCensored = {};
|
const isCensored = {};
|
||||||
const isMissing = {};
|
const isMissing = {};
|
||||||
const isSelected = {};
|
const isSelected = {};
|
||||||
@ -394,6 +395,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const txid of blockAudit.addedTxs) {
|
for (const txid of blockAudit.addedTxs) {
|
||||||
isAdded[txid] = true;
|
isAdded[txid] = true;
|
||||||
}
|
}
|
||||||
|
for (const txid of blockAudit.prioritizedTxs) {
|
||||||
|
isPrioritized[txid] = true;
|
||||||
|
}
|
||||||
for (const txid of blockAudit.missingTxs) {
|
for (const txid of blockAudit.missingTxs) {
|
||||||
isCensored[txid] = true;
|
isCensored[txid] = true;
|
||||||
}
|
}
|
||||||
@ -443,6 +447,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
tx.status = null;
|
tx.status = null;
|
||||||
} else if (isAdded[tx.txid]) {
|
} else if (isAdded[tx.txid]) {
|
||||||
tx.status = 'added';
|
tx.status = 'added';
|
||||||
|
} else if (isPrioritized[tx.txid]) {
|
||||||
|
tx.status = 'prioritized';
|
||||||
} else if (inTemplate[tx.txid]) {
|
} else if (inTemplate[tx.txid]) {
|
||||||
tx.status = 'found';
|
tx.status = 'found';
|
||||||
} else if (isRbf[tx.txid]) {
|
} else if (isRbf[tx.txid]) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
|
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
|
||||||
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
|
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
|
||||||
import { selectPowerOfTen } from '../../bitcoin.utils';
|
import { selectPowerOfTen } from '../../bitcoin.utils';
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
|
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
|
||||||
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
|
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface';
|
import { MempoolBlockDelta } from '../../interfaces/websocket.interface';
|
||||||
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||||
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
|
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
|
||||||
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
|
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
|
||||||
|
@ -3,7 +3,8 @@ import { detectWebGL } from '../../shared/graphs.utils';
|
|||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
||||||
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
|
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { Observable, BehaviorSubject } from 'rxjs';
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||||
|
@ -77,8 +77,9 @@
|
|||||||
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span>
|
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span>
|
||||||
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
|
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
|
||||||
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
|
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
|
||||||
<ng-template #notSeen><span class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
|
<ng-template #notSeen><span *ngIf="!auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
|
||||||
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added or prioritized out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
|
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
|
||||||
|
<span *ngIf="auditStatus.prioritized" class="badge badge-warning mr-1" i18n-ngbTooltip="Prioritized transaction tooltip" ngbTooltip="This transaction may have been prioritized out-of-band" placement="bottom" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
||||||
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,6 +42,7 @@ interface AuditStatus {
|
|||||||
seen?: boolean;
|
seen?: boolean;
|
||||||
expected?: boolean;
|
expected?: boolean;
|
||||||
added?: boolean;
|
added?: boolean;
|
||||||
|
prioritized?: boolean;
|
||||||
delayed?: number;
|
delayed?: number;
|
||||||
accelerated?: boolean;
|
accelerated?: boolean;
|
||||||
conflict?: boolean;
|
conflict?: boolean;
|
||||||
@ -317,13 +318,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
||||||
map(audit => {
|
map(audit => {
|
||||||
const isAdded = audit.addedTxs.includes(txid);
|
const isAdded = audit.addedTxs.includes(txid);
|
||||||
|
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||||
return {
|
return {
|
||||||
seen: isExpected || !(isAdded || isConflict),
|
seen: isExpected || isPrioritized || isAccelerated,
|
||||||
expected: isExpected,
|
expected: isExpected,
|
||||||
added: isAdded,
|
added: isAdded,
|
||||||
|
prioritized: isPrioritized,
|
||||||
conflict: isConflict,
|
conflict: isConflict,
|
||||||
accelerated: isAccelerated,
|
accelerated: isAccelerated,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
|
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
|
||||||
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||||
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
|
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg, TransactionStripped } from '../interfaces/node-api.interface';
|
||||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
import { MempoolInfo, 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';
|
||||||
|
@ -208,6 +208,7 @@ export interface BlockExtended extends Block {
|
|||||||
export interface BlockAudit extends BlockExtended {
|
export interface BlockAudit extends BlockExtended {
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
|
prioritizedTxs: string[],
|
||||||
freshTxs: string[],
|
freshTxs: string[],
|
||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
fullrbfTxs: string[],
|
fullrbfTxs: string[],
|
||||||
@ -231,7 +232,7 @@ export interface TransactionStripped {
|
|||||||
acc?: boolean;
|
acc?: boolean;
|
||||||
flags?: number | null;
|
flags?: number | null;
|
||||||
time?: number;
|
time?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SafeResourceUrl } from '@angular/platform-browser';
|
import { SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { ILoadingIndicators } from '../services/state.service';
|
import { ILoadingIndicators } from '../services/state.service';
|
||||||
import { Transaction } from './electrs.interface';
|
import { Transaction } from './electrs.interface';
|
||||||
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
|
import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
|
||||||
|
|
||||||
export interface WebsocketResponse {
|
export interface WebsocketResponse {
|
||||||
backend?: 'esplora' | 'electrum' | 'none';
|
backend?: 'esplora' | 'electrum' | 'none';
|
||||||
@ -93,19 +93,6 @@ export interface MempoolInfo {
|
|||||||
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
|
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionStripped {
|
|
||||||
txid: string;
|
|
||||||
fee: number;
|
|
||||||
vsize: number;
|
|
||||||
value: number;
|
|
||||||
acc?: boolean; // is accelerated?
|
|
||||||
rate?: number; // effective fee rate
|
|
||||||
flags?: number;
|
|
||||||
time?: number;
|
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
|
||||||
context?: 'projected' | 'actual';
|
|
||||||
}
|
|
||||||
|
|
||||||
// [txid, fee, vsize, value, rate, flags, acceleration?]
|
// [txid, fee, vsize, value, rate, flags, acceleration?]
|
||||||
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
|
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
|
||||||
// [txid, rate, flags, acceleration?]
|
// [txid, rate, flags, acceleration?]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
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 { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } 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';
|
||||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
|
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
|
||||||
|
import { TransactionStripped } from "../interfaces/node-api.interface";
|
||||||
|
|
||||||
export function isMobile(): boolean {
|
export function isMobile(): boolean {
|
||||||
return (window.innerWidth <= 767.98);
|
return (window.innerWidth <= 767.98);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user