diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 79d5ff2d1..a3714406f 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -55,7 +55,7 @@ class RbfCache { if (tree) { tree.interval = newTime - tree?.time; replacedTrees.push(tree); - fullRbf = fullRbf || tree.fullRbf; + fullRbf = fullRbf || tree.fullRbf || !tree.tx.rbf; } } } else { diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html index 68f8a1caf..540da7480 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html @@ -32,6 +32,7 @@ Status + Full RBF RBF RBF Mined diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts index b9da63c86..fc3748f32 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; -import { RbfInfo } from '../../interfaces/node-api.interface'; +import { RbfTree } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-rbf-timeline-tooltip', @@ -7,7 +7,7 @@ import { RbfInfo } from '../../interfaces/node-api.interface'; styleUrls: ['./rbf-timeline-tooltip.component.scss'], }) export class RbfTimelineTooltipComponent implements OnChanges { - @Input() rbfInfo: RbfInfo | void; + @Input() rbfInfo: RbfTree | null; @Input() cursorPosition: { x: number, y: number }; tooltipPosition = null; diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.html b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.html index ce5a9678f..a2012d45f 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.html +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.html @@ -15,14 +15,15 @@
- +
-
+
+
-
-
+
+
-
+
diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.scss b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.scss index 3745360a5..be7aef2d6 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.scss +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.scss @@ -83,15 +83,26 @@ transform: translateY(-50%); background: #105fb0; border-radius: 5px; + + &.left { + right: 50%; + } + &.right { + left: 50%; + } + + &.fullrbf { + background: #1bd8f4; + } } &.first-node { - .track { - left: 50%; + .track.left { + display: none; } } &:last-child { - .track { - right: 50%; + .track.right { + display: none; } } } @@ -177,11 +188,17 @@ height: 108px; bottom: 50%; border-right: solid 10px #105fb0; + &.fullrbf { + border-right: solid 10px #1bd8f4; + } } .corner { border-bottom: solid 10px #105fb0; border-bottom-right-radius: 10px; + &.fullrbf { + border-bottom: solid 10px #1bd8f4; + } } } } diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts index f02e8ca35..474da7326 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline.component.ts @@ -1,15 +1,20 @@ import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID, HostListener } from '@angular/core'; 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 { ApiService } from '../../services/api.service'; type Connector = 'pipe' | 'corner'; interface TimelineCell { - replacement?: RbfInfo, + replacement?: RbfTree, connector?: Connector, first?: boolean, + fullRbf?: boolean, +} + +function isTimelineCell(val: RbfTree | TimelineCell): boolean { + return !val || !('tx' in val); } @Component({ @@ -22,7 +27,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges { @Input() txid: string; rows: TimelineCell[][] = []; - hoverInfo: RbfInfo | void = null; + hoverInfo: RbfTree | null = null; tooltipPosition = null; dir: 'rtl' | 'ltr' = 'ltr'; @@ -53,13 +58,27 @@ export class RbfTimelineComponent implements OnInit, OnChanges { buildTimelines(tree: RbfTree): TimelineCell[][] { if (!tree) return []; + this.flagFullRbf(tree); const split = this.splitTimelines(tree); const timelines = this.prepareTimelines(split); 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 - splitTimelines(tree: RbfTree, tail: RbfInfo[] = []): RbfInfo[][] { + splitTimelines(tree: RbfTree, tail: RbfTree[] = []): RbfTree[][] { const replacements = [...tail, tree]; if (tree.replaces.length) { 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 // 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); const rows = lines.map(() => []); @@ -85,7 +104,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges { let emptyCount = 0; const nextGroups = []; for (const group of lineGroups) { - const toMerge: { [txid: string]: RbfInfo[][] } = {}; + const toMerge: { [txid: string]: RbfTree[][] } = {}; let emptyInGroup = 0; let first = true; for (const line of group) { @@ -97,7 +116,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges { } else { // substitute duplicates with empty cells // (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 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 - connectTimelines(timelines: RbfInfo[][]): TimelineCell[][] { + connectTimelines(timelines: (RbfTree | TimelineCell)[][]): TimelineCell[][] { const rows: TimelineCell[][] = []; timelines.forEach((lines, row) => { rows.push([]); @@ -135,11 +154,12 @@ export class RbfTimelineComponent implements OnInit, OnChanges { let finished = false; lines.forEach((replacement, column) => { const cell: TimelineCell = {}; - if (replacement) { - cell.replacement = replacement; + if (!isTimelineCell(replacement)) { + cell.replacement = replacement as RbfTree; + cell.fullRbf = (replacement as RbfTree).replacedBy?.fullRbf; } rows[row].push(cell); - if (replacement) { + if (!isTimelineCell(replacement)) { if (!started) { cell.first = true; started = true; @@ -153,11 +173,13 @@ export class RbfTimelineComponent implements OnInit, OnChanges { matched = true; } else if (i === row) { rows[i][column] = { - connector: 'corner' + connector: 'corner', + fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf, }; } else if (nextCell.connector !== 'corner') { rows[i][column] = { - connector: 'pipe' + connector: 'pipe', + fullRbf: (replacement as TimelineCell).replacement.tx.fullRbf, }; } } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 82e1ae50d..7a8ab3f06 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -39,6 +39,7 @@ export interface RbfTree extends RbfInfo { mined?: boolean; fullRbf: boolean; replaces: RbfTree[]; + replacedBy?: RbfTransaction; } export interface DifficultyAdjustment { @@ -176,9 +177,10 @@ export interface TransactionStripped { context?: 'projected' | 'actual'; } -interface RbfTransaction extends TransactionStripped { +export interface RbfTransaction extends TransactionStripped { rbf?: boolean; mined?: boolean, + fullRbf?: boolean, } export interface MempoolPosition { block: number,