Merge branch 'master' into mononaut/tracker-tx-routing

This commit is contained in:
wiz 2024-07-26 11:29:31 -05:00 committed by GitHub
commit adea897e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 245 additions and 9 deletions

View file

@ -165,6 +165,7 @@ class BitcoinRoutes {
acceleration: tx.acceleration,
acceleratedBy: tx.acceleratedBy || undefined,
acceleratedAt: tx.acceleratedAt || undefined,
feeDelta: tx.feeDelta || undefined,
});
return;
}

View file

@ -453,6 +453,7 @@ class MempoolBlocks {
mempoolTx.acceleration = true;
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
@ -460,6 +461,7 @@ class MempoolBlocks {
mempool[ancestor.txid].acceleration = true;
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
}
} else {

View file

@ -823,6 +823,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
@ -864,6 +865,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
if (!mempoolTx.cpfpChecked) {
calculateMempoolTxCpfp(mempoolTx, newMempool);
@ -1138,6 +1140,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
@ -1160,6 +1163,7 @@ class WebsocketHandler {
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
acceleratedAt: mempoolTx.acceleratedAt || undefined,
feeDelta: mempoolTx.feeDelta || undefined,
};
}
}

View file

@ -126,6 +126,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
replacement?: boolean;
uid?: number;
flags?: number;
@ -449,7 +450,7 @@ export interface OptimizedStatistic {
export interface TxTrackingInfo {
replacedBy?: string,
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
cpfp?: {
ancestors?: Ancestor[],
bestDescendant?: Ancestor | null,
@ -462,6 +463,7 @@ export interface TxTrackingInfo {
accelerated?: boolean,
acceleratedBy?: number[],
acceleratedAt?: number,
feeDelta?: number,
confirmed?: boolean
}

View file

@ -0,0 +1,62 @@
<div
#tooltip
*ngIf="accelerationInfo && tooltipPosition !== null"
class="acceleration-tooltip"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<table>
<tbody>
<tr>
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
<td class="value">
@if (accelerationInfo.status === 'seen') {
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
} @else if (accelerationInfo.status === 'accelerated') {
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
} @else if (accelerationInfo.status === 'mined') {
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
}
</td>
</tr>
<tr *ngIf="accelerationInfo.fee">
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
</tr>
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
@if (accelerationInfo.status === 'accelerated') {
<td style="color: #905cf4;" class="value">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
} @else {
<td style="color: #905cf4;" class="value">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
}
</tr>
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight && (accelerationInfo.feeDelta || accelerationInfo.bidBoost)">
@if (accelerationInfo.status === 'seen') {
<td class="label" i18n="transaction.fee-rate">Fee rate</td>
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
@if (accelerationInfo.status === 'accelerated') {
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
} @else {
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
}
}
</tr>
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
<td class="value" *ngIf="accelerationInfo.pools">
<ng-container *ngFor="let pool of accelerationInfo.pools">
<img *ngIf="accelerationInfo.poolsData[pool]"
class="pool-logo"
[class.highlight]="pool === accelerationInfo?.minedByPoolUniqueId"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
</ng-container>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,52 @@
.acceleration-tooltip {
position: fixed;
z-index: 3;
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: var(--tooltip-grey);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
pointer-events: none;
.badge.badge-accelerated {
background-color: var(--tertiary);
color: white;
}
.value {
text-align: end;
}
.label {
padding-right: 30px;
}
.pool-logo {
width: 22px;
height: 22px;
position: relative;
top: -1px;
margin-right: 3px;
}
.highlight {
filter: drop-shadow(0 0 5px #905cf4);
animation: pulse 1s infinite;
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

View file

@ -0,0 +1,38 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-acceleration-timeline-tooltip',
templateUrl: './acceleration-timeline-tooltip.component.html',
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
})
export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() accelerationInfo: any;
@Input() cursorPosition: { x: number, y: number };
tooltipPosition: any = null;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
let y = changes.cursorPosition.currentValue.y + 20;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
}
if (y + elementBounds.height > (window.innerHeight - 20)) {
y = y - elementBounds.height - 20;
}
}
this.tooltipPosition = { x, y };
}
}
hasPoolsData(): boolean {
return Object.keys(this.accelerationInfo.poolsData).length > 0;
}
}

View file

@ -26,7 +26,7 @@
<div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting">
<div class="shape animate"></div>
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div>
@ -58,7 +58,7 @@
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border">
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
@ -80,7 +80,7 @@
} @else {
<div class="seen-to-acc right"></div>
}
<div class="shape-border">
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div>
@if (!tx.status.confirmed) {
<div class="connector down loading"></div>
@ -113,7 +113,10 @@
} @else {
<div class="seen-to-acc left"></div>
}
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
<div class="shape-border"
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape"></div>
</div>
@if (tx.status.confirmed) {
@ -130,4 +133,10 @@
</div>
</div>
</div>
<app-acceleration-timeline-tooltip
[accelerationInfo]="hoverInfo"
[cursorPosition]="tooltipPosition"
></app-acceleration-timeline-tooltip>
</div>

View file

@ -152,9 +152,16 @@
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
cursor: pointer;
padding: 4px;
background: transparent;
transition: background-color 300ms, padding 300ms;
&.hovering {
cursor: pointer;
&:hover {
padding: 0px;
}
}
.shape {
position: relative;

View file

@ -1,6 +1,8 @@
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service';
@Component({
selector: 'app-acceleration-timeline',
@ -10,6 +12,7 @@ import { Transaction } from '../../interfaces/electrs.interface';
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
@Input() accelerationInfo: Acceleration;
@Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@ -22,13 +25,25 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
useAbsoluteTime: boolean = false;
interval: number;
constructor() {}
tooltipPosition = null;
hoverInfo: any = null;
poolsData: { [id: number]: SinglePoolStats } = {};
constructor(
private miningService: MiningService,
) {}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
this.miningService.getPools().subscribe(pools => {
for (const pool of pools) {
this.poolsData[pool.unique_id] = pool;
}
});
this.interval = window.setInterval(() => {
this.now = Math.floor(new Date().getTime() / 1000);
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
@ -52,4 +67,42 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
ngOnDestroy(): void {
clearInterval(this.interval);
}
onHover(event, status: string): void {
if (status === 'seen') {
this.hoverInfo = {
status,
fee: this.tx.fee,
weight: this.tx.weight
};
} else if (status === 'accelerated') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
weight: this.tx.weight,
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
} else if (status === 'mined') {
this.hoverInfo = {
status,
fee: this.accelerationInfo?.effectiveFee,
weight: this.tx.weight,
bidBoost: this.accelerationInfo?.bidBoost,
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
poolsData: this.poolsData
};
}
}
onBlur(event): void {
this.hoverInfo = null;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
}

View file

@ -167,7 +167,7 @@
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div>
<div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
<br>
</ng-container>

View file

@ -823,6 +823,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated(firstCpfp);
}

View file

@ -22,6 +22,7 @@ export interface Transaction {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;

View file

@ -31,6 +31,7 @@ export interface CpfpInfo {
acceleration?: boolean;
acceleratedBy?: number[];
acceleratedAt?: number;
feeDelta?: number;
}
export interface RbfInfo {

View file

@ -68,6 +68,7 @@ import { AddressTransactionsWidgetComponent } from '../components/address-transa
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
import { AccelerationTimelineComponent } from '../components/acceleration-timeline/acceleration-timeline.component';
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
import { AccelerationTimelineTooltipComponent } from '../components/acceleration-timeline/acceleration-timeline-tooltip.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
@ -180,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
AccelerationTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,
AssetsNavComponent,
@ -320,6 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent,
AccelerationTimelineTooltipComponent,
PushTransactionComponent,
TestTransactionsComponent,
AssetsNavComponent,