highlight & tag fullrbf replacements in RBF timeline
This commit is contained in:
parent
9cf961c667
commit
3287c62f91
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user