Merge remote-tracking branch 'origin/master' into mononaut/seo-ssr

This commit is contained in:
wiz
2023-08-05 18:23:12 +09:00
476 changed files with 57113 additions and 27203 deletions

View File

@@ -12,17 +12,18 @@ import {
tap
} from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, from, throwError } from 'rxjs';
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
import { isFeatureActive } from '../../bitcoin.utils';
@Component({
selector: 'app-transaction',
@@ -34,9 +35,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
tx: Transaction;
txId: string;
txInBlockIndex: number;
mempoolPosition: MempoolPosition;
isLoadingTx = true;
error: any = undefined;
errorUnblinded: any = undefined;
loadingCachedTx = false;
waitingForTransaction = false;
latestBlock: BlockExtended;
transactionTime = -1;
@@ -45,21 +48,25 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription;
txReplacedSubscription: Subscription;
blocksSubscription: Subscription;
txRbfInfoSubscription: Subscription;
mempoolPositionSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
mempoolBlocksSubscription: Subscription;
blocksSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
rbfReplaces: string[];
rbfInfo: RbfTree;
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
fetchCpfp$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
isCached: boolean = false;
now = new Date().getTime();
timeAvg$: Observable<number>;
now = Date.now();
da$: Observable<DifficultyAdjustment>;
liquidUnblinding = new LiquidUnblinding();
inputIndex: number;
outputIndex: number;
@@ -74,6 +81,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
flowEnabled: boolean;
blockConversion: Price;
tooltipPosition: { x: number, y: number };
isMobile: boolean;
featuresEnabled: boolean;
segwitEnabled: boolean;
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -83,7 +97,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
public stateService: StateService,
private cacheService: CacheService,
private websocketService: WebsocketService,
private audioService: AudioService,
@@ -104,11 +118,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setFlowEnabled();
});
this.timeAvg$ = timer(0, 1000)
.pipe(
switchMap(() => this.stateService.difficultyAdjustment$),
map((da) => da.timeAvg)
);
this.da$ = this.stateService.difficultyAdjustment$.pipe(
tap(() => {
this.now = Date.now();
})
);
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.fragmentParams = new URLSearchParams(fragment || '');
@@ -118,6 +132,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
});
this.blocksSubscription = this.stateService.blocks$.subscribe((blocks) => {
this.latestBlock = blocks[0];
});
this.fetchCpfpSubscription = this.fetchCpfp$
.pipe(
switchMap((txId) =>
@@ -145,6 +163,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
.subscribe((cpfpInfo) => {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
@@ -152,21 +171,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
if (!this.tx.status.confirmed) {
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
});
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
});
this.fetchRbfSubscription = this.fetchRbfHistory$
@@ -176,14 +198,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
.getRbfHistory$(txId)
),
catchError(() => {
return of([]);
return of(null);
})
).subscribe((replaces) => {
this.rbfReplaces = replaces;
).subscribe((rbfResponse) => {
this.rbfInfo = rbfResponse?.replacements;
this.rbfReplaces = rbfResponse?.replaces || null;
});
this.fetchCachedTxSubscription = this.fetchCachedTx$
.pipe(
tap(() => {
this.loadingCachedTx = true;
}),
switchMap((txId) =>
this.apiService
.getRbfCachedTx$(txId)
@@ -192,26 +218,50 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(null);
})
).subscribe((tx) => {
this.loadingCachedTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}
this.seoService.clearSoft404();
this.tx = tx;
this.isCached = true;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.setupGraph();
if (!this.tx) {
this.tx = tx;
this.setFeatures();
this.isCached = true;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.transactionTime = tx.firstSeen || 0;
this.setupGraph();
if (!this.tx?.status?.confirmed) {
this.fetchRbfHistory$.next(this.tx.txid);
this.txRbfInfoSubscription = this.stateService.txRbfInfo$.subscribe((rbfInfo) => {
if (this.tx) {
this.rbfInfo = rbfInfo;
}
});
}
});
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
this.now = Date.now();
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.mempoolPosition = txPosition.position;
if (this.tx && !this.tx.status.confirmed) {
this.stateService.markBlock$.next({
txid: txPosition.txid,
mempoolPosition: this.mempoolPosition
});
this.txInBlockIndex = this.mempoolPosition.block;
}
} else {
this.mempoolPosition = null;
}
});
@@ -252,7 +302,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
of(true),
this.stateService.connectionState$.pipe(
filter(
(state) => state === 2 && this.tx && !this.tx.status.confirmed
(state) => state === 2 && this.tx && !this.tx.status?.confirmed
)
)
);
@@ -288,13 +338,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
})
)
.subscribe((tx: Transaction) => {
if (!tx) {
this.seoService.logSoft404();
return;
}
this.seoService.clearSoft404();
if (!tx) {
this.fetchCachedTx$.next(this.txId);
this.seoService.logSoft404();
return;
}
this.seoService.clearSoft404();
this.tx = tx;
this.setFeatures();
this.isCached = false;
if (tx.fee === undefined) {
this.tx.fee = 0;
@@ -302,19 +354,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.loadingCachedTx = false;
this.waitingForTransaction = false;
this.setMempoolBlocksSubscription();
this.websocketService.startTrackTransaction(tx.txid);
this.graphExpanded = false;
this.setupGraph();
if (!tx.status.confirmed && tx.firstSeen) {
this.transactionTime = tx.firstSeen;
if (!tx.status?.confirmed) {
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
}
} else {
this.getTransactionTime();
this.transactionTime = 0;
}
if (this.tx.status.confirmed) {
if (this.tx?.status?.confirmed) {
this.stateService.markBlock$.next({
blockHeight: tx.status.block_height,
});
@@ -322,19 +378,23 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else {
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txid: tx.txid,
txFeePerVSize: tx.effectiveFeePerVsize,
mempoolPosition: this.mempoolPosition,
});
this.cpfpInfo = {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
const hasRelatives = !!(tx.ancestors?.length || tx.bestDescendant);
this.hasEffectiveFeeRate = hasRelatives || (tx.effectiveFeePerVsize && (Math.abs(tx.effectiveFeePerVsize - tx.feePerVsize) > 0.01));
} else {
this.fetchCpfp$.next(this.tx.txid);
}
this.fetchRbfHistory$.next(this.tx.txid);
}
this.fetchRbfHistory$.next(this.tx.txid);
this.priceService.getBlockPrice$(tx.status.block_time, true).pipe(
this.priceService.getBlockPrice$(tx.status?.block_time, true).pipe(
tap((price) => {
this.blockConversion = price;
})
@@ -349,10 +409,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
);
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx) {
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
this.tx.status = {
confirmed: true,
block_height: block.height,
@@ -367,6 +425,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
if (!this.tx) {
this.error = new Error();
this.loadingCachedTx = false;
this.waitingForTransaction = false;
}
this.rbfTransaction = rbfTransaction;
@@ -376,6 +435,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
});
this.txRbfInfoSubscription = this.stateService.txRbfInfo$.subscribe((rbfInfo) => {
if (this.tx) {
this.rbfInfo = rbfInfo;
}
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
if (params.showFlow === 'false') {
this.overrideFlowPreference = false;
@@ -387,6 +452,34 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setFlowEnabled();
this.setGraphSize();
});
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
this.now = Date.now();
if (!this.tx || this.mempoolPosition) {
return;
}
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
let found = false;
this.txInBlockIndex = 0;
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
if (
txFeePerVSize <= block.feeRange[i + 1] &&
txFeePerVSize >= block.feeRange[i]
) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
found = true;
}
}
}
if (!found && txFeePerVSize < mempoolBlocks[mempoolBlocks.length - 1].feeRange[0]) {
this.txInBlockIndex = 7;
}
});
}
ngAfterViewInit(): void {
@@ -404,47 +497,45 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return of(false);
}
setMempoolBlocksSubscription() {
this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (
txFeePerVSize <= block.feeRange[i + 1] &&
txFeePerVSize >= block.feeRange[i]
) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
}
}
}
});
}
getTransactionTime() {
this.apiService
.getTransactionTimes$([this.tx.txid])
.subscribe((transactionTimes) => {
this.transactionTime = transactionTimes[0];
if (transactionTimes?.length) {
this.transactionTime = transactionTimes[0];
}
});
}
setFeatures(): void {
if (this.tx) {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
} else {
this.segwitEnabled = false;
this.taprootEnabled = false;
this.rbfEnabled = false;
}
this.featuresEnabled = this.segwitEnabled || this.taprootEnabled || this.rbfEnabled;
}
resetTransaction() {
this.error = undefined;
this.tx = null;
this.setFeatures();
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.replaced = false;
this.transactionTime = -1;
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
this.rbfInfo = null;
this.rbfReplaces = [];
this.showCpfpDetails = false;
this.txInBlockIndex = null;
this.mempoolPosition = null;
document.body.scrollTo(0, 0);
this.leaveTransaction();
}
@@ -501,8 +592,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event'])
setGraphSize(): void {
if (this.graphContainer) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
this.isMobile = window.innerWidth < 850;
if (this.graphContainer?.nativeElement) {
setTimeout(() => {
if (this.graphContainer?.nativeElement) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
} else {
setTimeout(() => { this.setGraphSize(); }, 1);
}
}, 1);
}
}
@@ -512,10 +610,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.txRbfInfoSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
this.flowPrefSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.mempoolPositionSubscription.unsubscribe();
this.mempoolBlocksSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.leaveTransaction();
}
}