highlight & tag fullrbf replacements in RBF timeline
This commit is contained in:
		
							parent
							
								
									9cf961c667
								
							
						
					
					
						commit
						3287c62f91
					
				@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="td-width" i18n="transaction.status|Transaction Status">Status</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>
 | 
			
		||||
          <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>
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -15,14 +15,15 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nodes">
 | 
			
		||||
        <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"
 | 
			
		||||
              [id]="'node-'+cell.replacement.tx.txid"
 | 
			
		||||
              [class.selected]="txid === cell.replacement.tx.txid"
 | 
			
		||||
              [class.mined]="cell.replacement.tx.mined"
 | 
			
		||||
              [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"
 | 
			
		||||
                [class.rbf]="cell.replacement.tx.rbf"
 | 
			
		||||
                [routerLink]="['/tx/' | relativeUrl, cell.replacement.tx.txid]"
 | 
			
		||||
@ -36,14 +37,14 @@
 | 
			
		||||
          </ng-container>
 | 
			
		||||
          <ng-template #nonNode>
 | 
			
		||||
            <ng-container [ngSwitch]="cell.connector">
 | 
			
		||||
              <div class="connector" *ngSwitchCase="'pipe'"><div class="pipe"></div></div>
 | 
			
		||||
              <div class="connector" *ngSwitchCase="'corner'"><div class="corner"></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" [class.fullrbf]="cell.fullRbf"></div></div>
 | 
			
		||||
              <div class="node-spacer" *ngSwitchDefault></div>
 | 
			
		||||
            </ng-container>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <ng-container *ngIf="i < timeline.length - 1">
 | 
			
		||||
            <div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
 | 
			
		||||
              <div class="track"></div>
 | 
			
		||||
              <div class="track" [class.fullrbf]="cell.fullRbf"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </ng-container>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
                };
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user