Merge branch 'master' into natsoni/fix-network-errors

This commit is contained in:
softsimon
2024-04-02 14:31:32 +09:00
committed by GitHub
54 changed files with 5941 additions and 6488 deletions

View File

@@ -51,7 +51,7 @@
</div>
</div>
<ng-container *ngIf="address && transactions && transactions.length > 2">
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br>
<div class="box">
<div class="row">

View File

@@ -44,7 +44,7 @@ export class AddressComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private stateService: StateService,
public stateService: StateService,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService,

View File

@@ -14,6 +14,7 @@
[blockConversion]="blockConversion"
[filterFlags]="activeFilterFlags"
[filterMode]="filterMode"
[relativeTime]="relativeTime"
></app-block-overview-tooltip>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder">

View File

@@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
@@ -20,7 +20,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
@@ -46,6 +46,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() relativeTime: number | null;
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();

View File

@@ -1,6 +1,6 @@
import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
import { defaultColorFunction } from './utils';

View File

@@ -32,7 +32,8 @@ export default class TxView implements TransactionStripped {
rate?: number;
flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -53,6 +54,7 @@ export default class TxView implements TransactionStripped {
this.scene = scene;
this.context = tx.context;
this.txid = tx.txid;
this.time = tx.time || 0;
this.fee = tx.fee;
this.vsize = tx.vsize;
this.value = tx.value;

View File

@@ -45,7 +45,7 @@ export const defaultAuditColors = {
censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
prioritized: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'),
};
@@ -81,6 +81,8 @@ export function defaultColorFunction(
return auditColors.missing;
case 'added':
return auditColors.added;
case 'prioritized':
return auditColors.prioritized;
case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':

View File

@@ -14,6 +14,26 @@
<a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a>
</td>
</tr>
<tr *ngIf="time">
<ng-container [ngSwitch]="timeMode">
<ng-container *ngSwitchCase="'mempool'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="since" [time]="time" [fastRender]="true"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'missed'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="before" [time]="relativeTime - time"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'after'">
<td class="label" i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td class="value"><i><app-time kind="span" [time]="time - relativeTime"></app-time></i></td>
</ng-container>
<ng-container *ngSwitchCase="'mined'">
<td class="label" i18n="transaction.confirmed-after|Transaction confirmed after">Confirmed</td>
<td class="value"><i><app-time kind="span" [time]="relativeTime - time"></app-time></i></td>
</ng-container>
</ng-container>
</tr>
<tr>
<td class="label" i18n="dashboard.latest-transactions.amount">Amount</td>
<td class="value"><app-amount [blockConversion]="blockConversion" [satoshis]="value" [noFiat]="true"></app-amount></td>
@@ -54,6 +74,7 @@
<span *ngSwitchCase="'fresh'" class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span>
<span *ngSwitchCase="'freshcpfp'" class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span>
<span *ngSwitchCase="'added'" class="badge badge-warning" i18n="transaction.audit.added">Added</span>
<span *ngSwitchCase="'prioritized'" class="badge badge-warning" i18n="transaction.audit.prioritized">Prioritized</span>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@@ -3,6 +3,7 @@ import { Position } from '../../components/block-overview-graph/sprite-types.js'
import { Price } from '../../services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils';
import { Block } from '../../interfaces/electrs.interface.js';
@Component({
selector: 'app-block-overview-tooltip',
@@ -11,6 +12,7 @@ import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/fi
})
export class BlockOverviewTooltipComponent implements OnChanges {
@Input() tx: TransactionStripped | void;
@Input() relativeTime?: number;
@Input() cursorPosition: Position;
@Input() clickable: boolean;
@Input() auditEnabled: boolean = false;
@@ -19,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
@Input() filterMode: FilterMode = 'and';
txid = '';
time: number = 0;
fee = 0;
value = 0;
vsize = 1;
@@ -26,6 +29,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
effectiveRate;
acceleration;
hasEffectiveRate: boolean = false;
timeMode: 'mempool' | 'mined' | 'missed' | 'after' = 'mempool';
filters: Filter[] = [];
activeFilters: { [key: string]: boolean } = {};
@@ -56,6 +60,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
if (this.tx && (changes.tx || changes.filterFlags || changes.filterMode)) {
this.txid = this.tx.txid || '';
this.time = this.tx.time || 0;
this.fee = this.tx.fee || 0;
this.value = this.tx.value || 0;
this.vsize = this.tx.vsize || 1;
@@ -72,6 +77,22 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.activeFilters[filter.key] = true;
}
}
if (!this.relativeTime) {
this.timeMode = 'mempool';
} else {
if (this.tx?.context === 'actual' || this.tx?.status === 'found') {
this.timeMode = 'mined';
} else {
const time = this.relativeTime || Date.now();
if (this.time <= time) {
this.timeMode = 'missed';
} else {
this.timeMode = 'after';
}
}
}
this.cd.markForCheck();
}
}

View File

@@ -8,6 +8,7 @@
[orientation]="'top'"
[flip]="false"
[disableSpinner]="true"
[relativeTime]="block?.timestamp"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
</div>

View File

@@ -117,6 +117,7 @@
[blockConversion]="blockConversion"
[showFilters]="true"
[excludeFilters]="['replacement']"
[relativeTime]="block?.timestamp"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
@@ -232,7 +233,7 @@
<app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
[showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
@@ -247,7 +248,7 @@
<app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
[showFilters]="true" [excludeFilters]="['replacement']" [relativeTime]="block?.timestamp"></app-block-overview-graph>
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">

View File

@@ -371,6 +371,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isPrioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
@@ -394,6 +395,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
@@ -443,6 +447,8 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else if (isRbf[tx.txid]) {

View File

@@ -12,6 +12,7 @@
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
[relativeTime]="blockInfo[i]?.timestamp"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>

View File

@@ -1,6 +1,6 @@
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
import { selectPowerOfTen } from '../../bitcoin.utils';

View File

@@ -265,8 +265,8 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
type: 'value',
axisLabel: {
fontSize: 11,
formatter: (value) => {
return this.weightMode ? value * 4 : value;
formatter: (value): string => {
return this.weightMode ? (value * 4).toString() : value.toString();
}
},
splitLine: {

View File

@@ -1,7 +1,8 @@
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
import { StateService } from '../../services/state.service';
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface';
import { MempoolBlockDelta } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators';

View File

@@ -3,7 +3,8 @@ import { detectWebGL } from '../../shared/graphs.utils';
import { StateService } from '../../services/state.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';

View File

@@ -20,10 +20,12 @@
-
<app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate>
</div>
<div *ngIf="showMiningInfo" class="block-size">
<div *ngIf="showMiningInfo$ | async; else noMiningInfo" class="block-size">
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<ng-template #noMiningInfo>
<div class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
</ng-template>
<div [attr.data-cy]="'mempool-block-' + i + '-transaction-count'" class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { Subscription, Observable, of, combineLatest, BehaviorSubject } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { Router } from '@angular/router';
@@ -42,6 +42,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
mempoolBlocks$: Observable<MempoolBlock[]>;
difficultyAdjustments$: Observable<DifficultyAdjustment>;
loadingBlocks$: Observable<boolean>;
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
blocksSubscription: Subscription;
mempoolBlocksFull: MempoolBlock[] = [];
@@ -57,10 +58,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
network = '';
now = new Date().getTime();
timeOffset = 0;
showMiningInfo = false;
timeLtrSubscription: Subscription;
timeLtr: boolean;
showMiningInfoSubscription: Subscription;
animateEntry: boolean = false;
blockOffset: number = 155;
@@ -98,10 +97,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.widthChange.emit(this.mempoolWidth);
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.showMiningInfoSubscription = this.stateService.showMiningInfo$.subscribe((showMiningInfo) => {
this.showMiningInfo = showMiningInfo;
this.cd.markForCheck();
});
this.showMiningInfo$ = this.stateService.showMiningInfo$;
}
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
@@ -267,7 +263,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.chainTipSubscription.unsubscribe();
this.keySubscription.unsubscribe();
this.isTabHiddenSubscription.unsubscribe();
this.showMiningInfoSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout);
}

View File

@@ -411,7 +411,6 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
padding: [20, 0, 0, 0],
},
type: 'time',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
margin: 20,

View File

@@ -6,7 +6,7 @@
<span class="menu-click text-nowrap ellipsis">
<strong>
<span *ngIf="user.username.includes('@'); else usernamenospace">{{ user.username }}</span>
<ng-template #usernamenospace>@{{ user.username }}</ng-template>
<ng-template #usernamenospace>&#64;{{ user.username }}</ng-template>
</strong>
</span>
<span class="badge mr-1 badge-og" *ngIf="user.ogRank">

View File

@@ -23,7 +23,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() time: number;
@Input() dateString: number;
@Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain';
@Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' = 'plain';
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() relative = false;
@@ -86,7 +86,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
seconds = Math.floor(this.time);
}
if (seconds < 60) {
if (seconds < 1 && this.kind === 'span') {
return $localize`:@@date-base.immediately:Immediately`;
} else if (seconds < 60) {
if (this.relative || this.kind === 'since') {
return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until') {
@@ -206,6 +208,29 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
}
break;
case 'before':
if (number === 1) {
switch (unit) { // singular (1 day)
case 'year': return $localize`:@@time-span:${dateStrings.i18nYear}:DATE: before`; break;
case 'month': return $localize`:@@time-span:${dateStrings.i18nMonth}:DATE: before`; break;
case 'week': return $localize`:@@time-span:${dateStrings.i18nWeek}:DATE: before`; break;
case 'day': return $localize`:@@time-span:${dateStrings.i18nDay}:DATE: before`; break;
case 'hour': return $localize`:@@time-span:${dateStrings.i18nHour}:DATE: before`; break;
case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinute}:DATE: before`; break;
case 'second': return $localize`:@@time-span:${dateStrings.i18nSecond}:DATE: before`; break;
}
} else {
switch (unit) { // plural (2 days)
case 'year': return $localize`:@@time-span:${dateStrings.i18nYears}:DATE: before`; break;
case 'month': return $localize`:@@time-span:${dateStrings.i18nMonths}:DATE: before`; break;
case 'week': return $localize`:@@time-span:${dateStrings.i18nWeeks}:DATE: before`; break;
case 'day': return $localize`:@@time-span:${dateStrings.i18nDays}:DATE: before`; break;
case 'hour': return $localize`:@@time-span:${dateStrings.i18nHours}:DATE: before`; break;
case 'minute': return $localize`:@@time-span:${dateStrings.i18nMinutes}:DATE: before`; break;
case 'second': return $localize`:@@time-span:${dateStrings.i18nSeconds}:DATE: before`; break;
}
}
break;
default:
if (number === 1) {
switch (unit) { // singular (1 day)

View File

@@ -326,7 +326,7 @@
<br>
<p>If you have any questions about this Policy, would like to speak with us about the use of our Marks in ways not described in the Policy, or see any abuse of our Marks, please email us at &lt;legal@mempool.space&gt;</p>
<p>If you have any questions about this Policy, would like to speak with us about the use of our Marks in ways not described in the Policy, or see any abuse of our Marks, please email us at &lt;legal&#64;mempool.space&gt;</p>
</ol>

View File

@@ -77,8 +77,9 @@
<span *ngIf="auditStatus.coinbase; else expected" class="badge badge-primary mr-1" i18n="tx-features.tag.coinbase|Coinbase">Coinbase</span>
<ng-template #expected><span *ngIf="auditStatus.expected; else seen" class="badge badge-success mr-1" i18n-ngbTooltip="Expected in block tooltip" ngbTooltip="This transaction was projected to be included in the block" placement="bottom" i18n="tx-features.tag.expected|Expected in Block">Expected in Block</span></ng-template>
<ng-template #seen><span *ngIf="auditStatus.seen; else notSeen" class="badge badge-success mr-1" i18n-ngbTooltip="Seen in mempool tooltip" ngbTooltip="This transaction was seen in the mempool prior to mining" placement="bottom" i18n="tx-features.tag.seen|Seen in Mempool">Seen in Mempool</span></ng-template>
<ng-template #notSeen><span class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added or prioritized out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
<ng-template #notSeen><span *ngIf="!auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Not seen in mempool tooltip" ngbTooltip="This transaction was missing from our mempool prior to mining" placement="bottom" i18n="tx-features.tag.not-seen|Not seen in Mempool">Not seen in Mempool</span></ng-template>
<span *ngIf="auditStatus.added" class="badge badge-warning mr-1" i18n-ngbTooltip="Added transaction tooltip" ngbTooltip="This transaction may have been added out-of-band" placement="bottom" i18n="tx-features.tag.added|Added">Added</span>
<span *ngIf="auditStatus.prioritized" class="badge badge-warning mr-1" i18n-ngbTooltip="Prioritized transaction tooltip" ngbTooltip="This transaction may have been prioritized out-of-band" placement="bottom" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
<span *ngIf="auditStatus.conflict" class="badge badge-warning mr-1" i18n-ngbTooltip="Conflict in mempool tooltip" ngbTooltip="This transaction conflicted with another version in our mempool" placement="bottom" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
</ng-container>
</td>

View File

@@ -42,6 +42,7 @@ interface AuditStatus {
seen?: boolean;
expected?: boolean;
added?: boolean;
prioritized?: boolean;
delayed?: number;
accelerated?: boolean;
conflict?: boolean;
@@ -317,13 +318,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
map(audit => {
const isAdded = audit.addedTxs.includes(txid);
const isPrioritized = audit.prioritizedTxs.includes(txid);
const isAccelerated = audit.acceleratedTxs.includes(txid);
const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid);
return {
seen: isExpected || !(isAdded || isConflict),
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
};

View File

@@ -1,8 +1,8 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg, TransactionStripped } from '../interfaces/node-api.interface';
import { MempoolInfo, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service';

View File

@@ -208,6 +208,7 @@ export interface BlockExtended extends Block {
export interface BlockAudit extends BlockExtended {
missingTxs: string[],
addedTxs: string[],
prioritizedTxs: string[],
freshTxs: string[],
sigopTxs: string[],
fullrbfTxs: string[],
@@ -230,7 +231,8 @@ export interface TransactionStripped {
rate?: number; // effective fee rate
acc?: boolean;
flags?: number | null;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}

View File

@@ -1,9 +1,10 @@
import { SafeResourceUrl } from '@angular/platform-browser';
import { ILoadingIndicators } from '../services/state.service';
import { Transaction } from './electrs.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree } from './node-api.interface';
import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface';
export interface WebsocketResponse {
backend?: 'esplora' | 'electrum' | 'none';
block?: BlockExtended;
blocks?: BlockExtended[];
conversions?: any;
@@ -92,20 +93,8 @@ export interface MempoolInfo {
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
}
export interface TransactionStripped {
txid: string;
fee: number;
vsize: number;
value: number;
acc?: boolean; // is accelerated?
rate?: number; // effective fee rate
flags?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}
// [txid, fee, vsize, value, rate, flags, acceleration?]
export type TransactionCompressed = [string, number, number, number, number, number, 1?];
export type TransactionCompressed = [string, number, number, number, number, number, number, 1?];
// [txid, rate, flags, acceleration?]
export type MempoolDeltaChange = [string, number, number, (1|0)];

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { Inject, Injectable, PLATFORM_ID, makeStateKey, TransferState } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformBrowser } from '@angular/common';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface';
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
@@ -92,6 +92,7 @@ const defaultEnv: Env = {
export class StateService {
isBrowser: boolean = isPlatformBrowser(this.platformId);
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
network = '';
lightning = false;
blockVSize: number;
@@ -99,6 +100,7 @@ export class StateService {
latestBlockHeight = -1;
blocks: BlockExtended[] = [];
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1);
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
@@ -257,6 +259,10 @@ export class StateService {
const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
this.backend$.subscribe(backend => {
this.backend = backend;
});
}
setNetworkBasedonUrl(url: string) {

View File

@@ -62,6 +62,7 @@ export class WebsocketService {
if (theInitData.body.blocks) {
theInitData.body.blocks = theInitData.body.blocks.reverse();
}
this.stateService.backend$.next(theInitData.backend);
this.stateService.isLoadingWebSocket$.next(false);
this.handleResponse(theInitData.body);
this.startSubscription(false, true);
@@ -290,6 +291,10 @@ export class WebsocketService {
handleResponse(response: WebsocketResponse) {
let reinitBlocks = false;
if (response.backend) {
this.stateService.backend$.next(response.backend);
}
if (response.blocks && response.blocks.length) {
const blocks = response.blocks;
this.stateService.resetBlocks(blocks);

View File

@@ -1,4 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed, TransactionStripped } from "../interfaces/websocket.interface";
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
import { TransactionStripped } from "../interfaces/node-api.interface";
export function isMobile(): boolean {
return (window.innerWidth <= 767.98);
@@ -164,7 +165,8 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
value: tx[3],
rate: tx[4],
flags: tx[5],
acc: !!tx[6],
time: tx[6],
acc: !!tx[7],
};
}