highlight & tag fullrbf replacements in RBF timeline

This commit is contained in:
Mononaut 2023-07-13 10:42:33 +09:00
parent 9cf961c667
commit 3287c62f91
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
7 changed files with 69 additions and 26 deletions

View File

@ -55,7 +55,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; fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf;
} }
} }
} else { } else {

View File

@ -32,6 +32,7 @@
<tr> <tr>
<td class="td-width" i18n="transaction.status|Transaction Status">Status</td> <td class="td-width" i18n="transaction.status|Transaction Status">Status</td>
<td> <td>
<span *ngIf="rbfInfo.tx.fullRbf" class="badge badge-info" i18n="rbfInfo-features.tag.full-rbf|Full RBF">Full RBF</span>
<span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span> <span *ngIf="rbfInfo.tx.rbf; else rbfDisabled" class="badge badge-success" i18n="rbfInfo-features.tag.rbf|RBF">RBF</span>
<ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template> <ng-template #rbfDisabled><span class="badge badge-danger mr-1"><del i18n="rbfInfo-features.tag.rbf|RBF">RBF</del></span></ng-template>
<span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> <span *ngIf="rbfInfo.tx.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
import { RbfInfo } from '../../interfaces/node-api.interface'; import { RbfTree } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-rbf-timeline-tooltip', selector: 'app-rbf-timeline-tooltip',
@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface';
styleUrls: ['./rbf-timeline-tooltip.component.scss'], styleUrls: ['./rbf-timeline-tooltip.component.scss'],
}) })
export class RbfTimelineTooltipComponent implements OnChanges { export class RbfTimelineTooltipComponent implements OnChanges {
@Input() rbfInfo: RbfInfo | void; @Input() rbfInfo: RbfTree | null;
@Input() cursorPosition: { x: number, y: number }; @Input() cursorPosition: { x: number, y: number };
tooltipPosition = null; tooltipPosition = null;

View File

@ -15,14 +15,15 @@
</div> </div>
<div class="nodes"> <div class="nodes">
<ng-container *ngFor="let cell of timeline; let i = index;"> <ng-container *ngFor="let cell of timeline; let i = index;">
<ng-container *ngIf="cell.replacement; else nonNode"> <ng-container *ngIf="cell.replacement?.tx; else nonNode">
<div class="node" <div class="node"
[id]="'node-'+cell.replacement.tx.txid" [id]="'node-'+cell.replacement.tx.txid"
[class.selected]="txid === cell.replacement.tx.txid" [class.selected]="txid === cell.replacement.tx.txid"
[class.mined]="cell.replacement.tx.mined" [class.mined]="cell.replacement.tx.mined"
[class.first-node]="cell.first" [class.first-node]="cell.first"
> >
<div class="track"></div> <div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
<a class="shape-border" <a class="shape-border"
[class.rbf]="cell.replacement.tx.rbf" [class.rbf]="cell.replacement.tx.rbf"
[routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]" [routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
@ -36,14 +37,14 @@
</ng-container> </ng-container>
<ng-template #nonNode> <ng-template #nonNode>
<ng-container [ngSwitch]="cell.connector"> <ng-container [ngSwitch]="cell.connector">
<div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div> <div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="connector" *ngSwitchCase="'corner'"><div class="corner"></div></div> <div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="node-spacer" *ngSwitchDefault></div> <div class="node-spacer" *ngSwitchDefault></div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-container *ngIf="i < timeline.length - 1"> <ng-container *ngIf="i < timeline.length - 1">
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer"> <div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
<div class="track"></div> <div class="track" [class.fullrbf]="cell.fullRbf"></div>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@ -83,15 +83,26 @@
transform: translateY(-50%); transform: translateY(-50%);
background: #105fb0; background: #105fb0;
border-radius: 5px; border-radius: 5px;
&.left {
right: 50%;
}
&.right {
left: 50%;
}
&.fullrbf {
background: #1bd8f4;
}
} }
&.first-node { &.first-node {
.track { .track.left {
left: 50%; display: none;
} }
} }
&:last-child { &:last-child {
.track { .track.right {
right: 50%; display: none;
} }
} }
} }
@ -177,11 +188,17 @@
height: 108px; height: 108px;
bottom: 50%; bottom: 50%;
border-right: solid 10px #105fb0; border-right: solid 10px #105fb0;
&.fullrbf {
border-right: solid 10px #1bd8f4;
}
} }
.corner { .corner {
border-bottom: solid 10px #105fb0; border-bottom: solid 10px #105fb0;
border-bottom-right-radius: 10px; border-bottom-right-radius: 10px;
&.fullrbf {
border-bottom: solid 10px #1bd8f4;
}
} }
} }
} }

View File

@ -1,15 +1,20 @@
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RbfInfo, RbfTree } from '../../interfaces/node-api.interface'; import { RbfTree, RbfTransaction } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
type Connector = 'pipe' | 'corner'; type Connector = 'pipe' | 'corner';
interface TimelineCell { interface TimelineCell {
replacement?: RbfInfo, replacement?: RbfTree,
connector?: Connector, connector?: Connector,
first?: boolean, first?: boolean,
fullRbf?: boolean,
}
function isTimelineCell(val: RbfTree | TimelineCell): boolean {
return !val || !('tx' in val);
} }
@Component({ @Component({
@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
@Input() txid: string; @Input() txid: string;
rows: TimelineCell[][] = []; rows: TimelineCell[][] = [];
hoverInfo: RbfInfo | void = null; hoverInfo: RbfTree | null = null;
tooltipPosition = null; tooltipPosition = null;
dir: 'rtl' | 'ltr' = 'ltr'; dir: 'rtl' | 'ltr' = 'ltr';
@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
buildTimelines(tree: RbfTree): TimelineCell[][] { buildTimelines(tree: RbfTree): TimelineCell[][] {
if (!tree) return []; if (!tree) return [];
this.flagFullRbf(tree);
const split = this.splitTimelines(tree); const split = this.splitTimelines(tree);
const timelines = this.prepareTimelines(split); const timelines = this.prepareTimelines(split);
return this.connectTimelines(timelines); return this.connectTimelines(timelines);
} }
// sets the fullRbf flag on each transaction in the tree
flagFullRbf(tree: RbfTree): void {
let fullRbf = false;
for (const replaced of tree.replaces) {
if (!replaced.tx.rbf) {
fullRbf = true;
}
replaced.replacedBy = tree.tx;
this.flagFullRbf(replaced);
}
tree.tx.fullRbf = fullRbf;
}
// splits a tree into N leaf-to-root paths // splits a tree into N leaf-to-root paths
splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] { splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] {
const replacements = [...tail, tree]; const replacements = [...tail, tree];
if (tree.replaces.length) { if (tree.replaces.length) {
return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements))); return [].concat(...tree.replaces.map(subtree => this.splitTimelines(subtree, replacements)));
@ -70,7 +89,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
// merges separate leaf-to-root paths into a coherent forking timeline // merges separate leaf-to-root paths into a coherent forking timeline
// represented as a 2D array of Rbf events // represented as a 2D array of Rbf events
prepareTimelines(lines: RbfInfo[][]): RbfInfo[][] { prepareTimelines(lines: RbfTree[][]): (RbfTree | TimelineCell)[][] {
lines.sort((a, b) => b.length - a.length); lines.sort((a, b) => b.length - a.length);
const rows = lines.map(() => []); const rows = lines.map(() => []);
@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
let emptyCount = 0; let emptyCount = 0;
const nextGroups = []; const nextGroups = [];
for (const group of lineGroups) { for (const group of lineGroups) {
const toMerge: { [txid: string]: RbfInfo[][] } = {}; const toMerge: { [txid: string]: RbfTree[][] } = {};
let emptyInGroup = 0; let emptyInGroup = 0;
let first = true; let first = true;
for (const line of group) { for (const line of group) {
@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
} else { } else {
// substitute duplicates with empty cells // substitute duplicates with empty cells
// (we'll fill these in with connecting lines later) // (we'll fill these in with connecting lines later)
rows[index].unshift(null); rows[index].unshift({ connector: true, replacement: head });
} }
// group the tails of the remaining lines for the next iteration // group the tails of the remaining lines for the next iteration
if (line.length) { if (line.length) {
@ -127,7 +146,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
} }
// annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements // annotates a 2D timeline array with info needed to draw connecting lines for multi-replacements
connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] { connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] {
const rows: TimelineCell[][] = []; const rows: TimelineCell[][] = [];
timelines.forEach((lines, row) => { timelines.forEach((lines, row) => {
rows.push([]); rows.push([]);
@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
let finished = false; let finished = false;
lines.forEach((replacement, column) => { lines.forEach((replacement, column) => {
const cell: TimelineCell = {}; const cell: TimelineCell = {};
if (replacement) { if (!isTimelineCell(replacement)) {
cell.replacement = replacement; cell.replacement = replacement as RbfTree;
cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf;
} }
rows[row].push(cell); rows[row].push(cell);
if (replacement) { if (!isTimelineCell(replacement)) {
if (!started) { if (!started) {
cell.first = true; cell.first = true;
started = true; started = true;
@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
matched = true; matched = true;
} else if (i === row) { } else if (i === row) {
rows[i][column] = { rows[i][column] = {
connector: 'corner' connector: 'corner',
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
}; };
} else if (nextCell.connector !== 'corner') { } else if (nextCell.connector !== 'corner') {
rows[i][column] = { rows[i][column] = {
connector: 'pipe' connector: 'pipe',
fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf,
}; };
} }
} }

View File

@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo {
mined?: boolean; mined?: boolean;
fullRbf: boolean; fullRbf: boolean;
replaces: RbfTree[]; replaces: RbfTree[];
replacedBy?: RbfTransaction;
} }
export interface DifficultyAdjustment { export interface DifficultyAdjustment {
@ -176,9 +177,10 @@ export interface TransactionStripped {
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }
interface RbfTransaction extends TransactionStripped { export interface RbfTransaction extends TransactionStripped {
rbf?: boolean; rbf?: boolean;
mined?: boolean, mined?: boolean,
fullRbf?: boolean,
} }
export interface MempoolPosition { export interface MempoolPosition {
block: number, block: number,