This commit is contained in:
natsoni 2026-08-07 11:34:11 +07:00 committed by GitHub
commit b1e515cd8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 675 additions and 10 deletions

View file

@ -448,7 +448,11 @@
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
@if (startedPastSummary) {
<button type="button" class="btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="transaction.view-details|View Transaction Details">View Transaction Details</button>
} @else {
<button type="button" class="btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
}
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile' || step === 'bitcoin') {
@ -607,7 +611,13 @@
<hr>
<div class="row mt-2 mb-2 text-center">
<div class="col-sm d-flex flex-column">
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="close">Close</button>
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()">
@if (startedPastSummary) {
<span i18n="transaction.view-transaction|View Transaction">View Transaction</span>
} @else {
<span i18n="close">Close</span>
}
</button>
</div>
</div>
}

View file

@ -71,6 +71,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@Input() noCTA: boolean = false;
@Input() initialStep: CheckoutStep = 'summary';
@Input() partnerCode: string | undefined;
@Output() unavailable = new EventEmitter<boolean>();
@Output() completed = new EventEmitter<boolean>();
@ -169,7 +170,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary', true);
this.moveToStep(this.initialStep, true);
} else {
this.auth = auth;
}
@ -182,7 +183,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.insertSquare();
this.setupSquare();
} else {
this.moveToStep('summary', true);
this.moveToStep(this.initialStep, true);
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
@ -237,9 +238,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (this._step === 'checkout') {
this.insertSquare();
this.enterpriseService.goal(8);
this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100);
if (!this.startedPastSummary) {
this.scrollToElementWithTimeout('acceleratePreviewAnchor', 'start', 100);
}
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.invoice = undefined;
@ -330,14 +333,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tap((response) => {
if (response.status === 204) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
if (this.step === 'summary' || this.startedPastSummary) {
this.unavailable.emit(true);
}
} else {
this.estimate = response.body;
if (!this.estimate) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
if (this.step === 'summary' || this.startedPastSummary) {
this.unavailable.emit(true);
}
this.estimate$.next(null);
@ -364,7 +367,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!this.couldPay) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
if (this.step === 'summary' || this.startedPastSummary) {
this.unavailable.emit(true);
}
return;
@ -383,7 +386,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.estimate = undefined;
this.quoteError = `cannot_accelerate_tx`;
this.estimateSubscription.unsubscribe();
if (this.step === 'summary') {
if (this.step === 'summary' || this.startedPastSummary) {
this.unavailable.emit(true);
} else {
this.accelerateError = 'cannot_accelerate_tx';
@ -1031,6 +1034,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return this._step;
}
get startedPastSummary(): boolean {
return this.initialStep !== 'summary';
}
get paymentMethods(): PaymentMethod[] {
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
}

View file

@ -9,6 +9,13 @@ import { TrackerBarComponent } from '@components/tracker/tracker-bar.component';
import { TransactionModule } from '@components/transaction/transaction.module';
const routes: Routes = [
{
path: ':id/accelerate',
component: TrackerComponent,
data: {
ogImage: true
}
},
{
path: ':id',
component: TrackerComponent,

View file

@ -0,0 +1,72 @@
<div class="container-xl">
<span id="accelerate" #accelerate class="anchor"></span>
<div class="title-block">
<div class="title mb-1">
<h2>
<a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">
<app-svg-images name="accelerator" [height]="isMobile ? '35px' : '45px'"></app-svg-images>
</a>
</h2>
</div>
<button *ngIf="hasAccelerationDetails" class="btn btn-sm btn-outline-info details-button ms-2" (click)="showAccelerationDetails = !showAccelerationDetails" i18n="transaction.details|Transaction Details">Details</button>
</div>
@if (waitingForTransaction) {
<div class="text-center">
<h3 i18n="transaction.error.transaction-not-found">Transaction not found.</h3>
<h5 i18n="transaction.error.waiting-for-it-to-appear">Waiting for it to appear in the mempool...</h5>
<div class="spinner-border text-light mt-2"></div>
</div>
} @else if (error && !loadingCachedTx) {
<app-http-error [error]="error">
<span i18n="transaction.error.loading-transaction-data">Error loading transaction data.</span>
</app-http-error>
} @else if (acceleratorCheckoutLoading) {
<ng-container *ngTemplateOutlet="acceleratorCheckoutLoadingWidget"></ng-container>
} @else {
@if (showAccelerationSummary) {
<app-accelerate-checkout
*ngIf="(da$ | async) && (ETA$ | async) as eta; else acceleratorCheckoutLoadingWidget"
[advancedEnabled]="true"
[tx]="tx"
[accelerating]="isAcceleration"
[eta]="eta"
[miningStats]="miningStats"
[showDetails]="showAccelerationDetails"
[noCTA]="true"
[initialStep]="'checkout'"
[partnerCode]="partnerCode"
(hasDetails)="setHasAccelerationDetails($event)"
(completed)="onAccelerationCompleted()"
(unavailable)="onAcceleratorUnavailable()"
class="h-100 w-100"
></app-accelerate-checkout>
} @else {
<ng-container *ngTemplateOutlet="acceleratorUnavailable"></ng-container>
}
}
<ng-template #acceleratorCheckoutLoadingWidget>
<div class="box card w-100 accelerate-checkout-inner" style="background: var(--box-bg)">
<div class="row">
<div class="col-md">
<div class="d-flex flex-row justify-content-center align-items-center">
<div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
</div>
</div>
</div>
</ng-template>
<ng-template #acceleratorUnavailable>
<div class="box w-100" style="background: var(--box-bg)">
<app-mempool-error [error]="acceleratorError"></app-mempool-error>
<div class="text-center">
<a class="btn btn-secondary btn-sm rounded-pill" [routerLink]="['/tx/' | relativeUrl, txId]" queryParamsHandling="merge">
<span i18n="transaction.view-transaction|View Transaction">View Transaction</span>
</a>
</div>
</div>
</ng-template>
</div>

View file

@ -0,0 +1,559 @@
import { Component, HostListener, Inject, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { of, merge, Subscription, Observable, Subject, throwError, combineLatest, BehaviorSubject } from 'rxjs';
import { catchError, delay, distinctUntilChanged, filter, map, mergeMap, retryWhen, startWith, switchMap, tap } from 'rxjs/operators';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { TransactionFlags } from '@app/shared/filters.utils';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { getTransactionFlags, getUnacceleratedFeeRate } from '@app/shared/transaction.utils';
import { ApiService } from '@app/services/api.service';
import { CacheService } from '@app/services/cache.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { ETA, EtaService } from '@app/services/eta.service';
import { MiningService, MiningStats } from '@app/services/mining.service';
import { PartnerCodeService } from '@app/services/partner-code.service';
import { SeoService } from '@app/services/seo.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition } from '@interfaces/node-api.interface';
@Component({
selector: 'app-transaction-accelerate',
templateUrl: './transaction-accelerate.component.html',
styleUrls: ['./transaction.component.scss'],
standalone: false,
})
export class TransactionAccelerateComponent implements OnInit, OnDestroy {
network = '';
tx: Transaction;
txId: string;
txInBlockIndex: number;
mempoolPosition: MempoolPosition;
accelerationPositions: AccelerationPosition[];
isLoadingTx = true;
loadingCachedTx = false;
waitingForTransaction = false;
error: any = undefined;
isCached = false;
replaced = false;
miningStats: MiningStats;
da$: Observable<DifficultyAdjustment>;
ETA$: Observable<ETA | null>;
eligibleForAcceleration = false;
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
isAcceleration = false;
notAcceleratedOnLoad: boolean = null;
showAccelerationDetails = false;
hasAccelerationDetails = false;
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
isMobile: boolean;
partnerCode: string | undefined;
private subscription: Subscription;
private fetchCpfpSubscription: Subscription;
private fetchCachedTxSubscription: Subscription;
private mempoolPositionSubscription: Subscription;
private networkChangedSubscription: Subscription;
private txConfirmedSubscription: Subscription;
private txReplacedSubscription: Subscription;
private urlFragmentSubscription: Subscription;
private partnerCodeSubscription: Subscription;
private fetchCpfp$ = new Subject<string>();
private fetchCachedTx$ = new Subject<string>();
private txChanged$ = new BehaviorSubject<boolean>(false);
private isAccelerated$ = new BehaviorSubject<boolean>(false);
constructor(
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService,
public stateService: StateService,
private cacheService: CacheService,
private websocketService: WebsocketService,
private apiService: ApiService,
private seoService: SeoService,
private enterpriseService: EnterpriseService,
private partnerCodeService: PartnerCodeService,
private miningService: MiningService,
private etaService: EtaService,
@Inject(ZONE_SERVICE) private zoneService: any,
) {}
ngOnInit(): void {
this.onResize();
this.setupPartnerCode();
this.enterpriseService.page();
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.consumePartnerCodeFragment(fragment);
});
if (!this.stateService.isLiquid()) {
this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningStats = stats;
this.isAccelerated$.next(this.isAcceleration);
});
}
this.websocketService.want(['blocks', 'mempool-blocks']);
this.networkChangedSubscription = this.stateService.networkChanged$.subscribe((network) => {
this.network = network;
this.acceleratorAvailable = this.stateService.env.ACCELERATOR_BUTTON && this.stateService.network === '';
});
this.da$ = this.stateService.difficultyAdjustment$.pipe(
tap(() => {
this.txChanged$.next(!!this.tx);
})
);
this.fetchCpfpSubscription = this.fetchCpfp$
.pipe(
switchMap((txId) =>
this.apiService
.getCpfpinfo$(txId)
.pipe(retryWhen((errors) => errors.pipe(
mergeMap((error) => {
if (!this.tx?.status || this.tx.status.confirmed) {
return throwError(() => error);
}
return of(null);
}),
delay(2000)
)),
catchError(() => {
return of(null);
})
)
),
catchError(() => {
return of(null);
})
)
.subscribe((cpfpInfo) => {
this.setCpfpInfo(cpfpInfo);
});
this.fetchCachedTxSubscription = this.fetchCachedTx$
.pipe(
tap(() => {
this.loadingCachedTx = true;
}),
switchMap((txId) =>
this.apiService
.getRbfCachedTx$(txId)
.pipe(catchError(() => {
return of(null);
}))
)
)
.subscribe((tx) => {
this.loadingCachedTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}
this.seoService.clearSoft404();
this.setTransaction(tx, true);
});
this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.mempoolPosition = txPosition.position;
this.accelerationPositions = txPosition.accelerationPositions;
if (this.tx && !this.tx.status?.confirmed) {
const txFeePerVSize = getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
this.stateService.markBlock$.next({
txid: txPosition.txid,
txFeePerVSize,
mempoolPosition: this.mempoolPosition,
accelerationPositions: this.accelerationPositions,
});
this.txInBlockIndex = this.mempoolPosition.block;
if (txPosition.cpfp !== undefined) {
this.setCpfpInfo(txPosition.cpfp);
}
if (txPosition.position.accelerated) {
this.tx.acceleration = true;
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
this.tx.acceleratedAt = txPosition.position.acceleratedAt;
this.setIsAccelerated();
} else {
this.markAccelerationStateLoaded();
}
}
} else {
this.mempoolPosition = null;
this.accelerationPositions = null;
}
});
this.subscription = this.zoneService.wrapObservable(this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
const urlMatch = (params.get('id') || '').split(':');
this.txId = urlMatch.length === 2 && urlMatch[1].length === 64 ? urlMatch[1] : urlMatch[0];
this.seoService.setTitle(
$localize`:@@transaction.accelerate.title:Accelerate Transaction: ${this.txId}:INTERPOLATION:`
);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction.accelerate:Accelerate Bitcoin transaction with txid ${this.txId} using Mempool Accelerator.`);
this.resetTransaction();
return merge(
of(true),
this.stateService.connectionState$.pipe(
filter(
(state) => state === 2 && this.tx && !this.tx.status?.confirmed
)
)
);
}),
switchMap(() => {
let transactionObservable$: Observable<Transaction>;
const cached = this.cacheService.getTxFromCache(this.txId);
if (cached && cached.fee !== -1) {
transactionObservable$ = of(cached);
} else {
transactionObservable$ = this.electrsApiService
.getTransaction$(this.txId)
.pipe(
catchError(this.handleLoadElectrsTransactionError.bind(this))
);
}
return merge(
transactionObservable$,
this.stateService.mempoolTransactions$
);
}),
))
.subscribe((tx: Transaction) => {
if (!tx) {
this.fetchCachedTx$.next(this.txId);
this.seoService.logSoft404();
return;
}
if (tx.txid && tx.txid !== this.txId) {
return;
}
this.seoService.clearSoft404();
this.setTransaction(tx, false);
},
(error) => {
this.error = error;
this.seoService.logSoft404();
this.isLoadingTx = false;
}
);
this.txConfirmedSubscription = 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,
block_hash: block.id,
block_time: block.timestamp,
};
this.txChanged$.next(true);
this.stateService.markBlock$.next({ blockHeight: block.height });
}
});
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
if (!this.tx) {
this.error = new Error();
this.loadingCachedTx = false;
this.waitingForTransaction = false;
}
this.replaced = true;
this.stateService.markBlock$.next({});
if (rbfTransaction && !this.tx) {
this.fetchCachedTx$.next(this.txId);
}
});
this.ETA$ = combineLatest([
this.stateService.mempoolTxPosition$.pipe(startWith(null)),
this.stateService.mempoolBlocks$.pipe(startWith(null)),
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
this.isAccelerated$,
this.txChanged$,
]).pipe(
map(([position, mempoolBlocks, da, isAccelerated]) => {
if (!this.tx || this.tx.status?.confirmed || !position || position.txid !== this.tx.txid) {
return null;
}
return this.etaService.calculateETA(
this.network,
this.tx,
mempoolBlocks,
position,
da,
this.miningStats,
isAccelerated,
this.accelerationPositions,
);
}),
distinctUntilChanged((prev: ETA | null, curr: ETA | null) => {
return prev === curr || (prev && curr && prev.time === curr.time && prev.blocks === curr.blocks);
})
);
}
private setTransaction(tx: Transaction, cached: boolean): void {
this.tx = tx;
this.isCached = cached;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.checkAccelerationEligibility();
this.txChanged$.next(true);
this.isLoadingTx = false;
this.error = undefined;
this.loadingCachedTx = false;
this.waitingForTransaction = false;
if (!tx.status?.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
if (tx.cpfpChecked) {
this.setCpfpInfo({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
});
} else {
this.fetchCpfp$.next(this.tx.txid);
}
} else {
this.notAcceleratedOnLoad = false;
this.stateService.markBlock$.next({
blockHeight: tx.status.block_height,
});
}
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
this.websocketService.startMultiTrackTransaction(this.txId);
this.waitingForTransaction = true;
}
this.error = error;
this.seoService.logSoft404();
this.isLoadingTx = false;
return of(false);
}
setCpfpInfo(cpfpInfo: CpfpInfo | null): void {
if (!this.tx) {
return;
}
if (!cpfpInfo) {
this.markAccelerationStateLoaded();
return;
}
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
if (!cpfpInfo.effectiveFeePerVsize && relatives.length) {
const totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
const totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize || this.tx.effectiveFeePerVsize || this.tx.feePerVsize || (this.tx.fee / (this.tx.weight / 4));
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.tx.acceleratedAt = cpfpInfo.acceleratedAt;
this.tx.feeDelta = cpfpInfo.feeDelta;
this.setIsAccelerated();
} else {
this.markAccelerationStateLoaded();
}
this.txChanged$.next(true);
}
private setIsAccelerated(): void {
this.isAcceleration = !!(this.tx?.acceleration || this.mempoolPosition?.accelerated);
if (this.isAcceleration) {
this.miningService.getMiningStats('1m').subscribe(stats => {
this.miningStats = stats;
this.isAccelerated$.next(this.isAcceleration);
});
}
this.isAccelerated$.next(this.isAcceleration);
this.markAccelerationStateLoaded();
}
private markAccelerationStateLoaded(): void {
if (this.notAcceleratedOnLoad === null) {
this.notAcceleratedOnLoad = !this.isAcceleration;
}
}
checkAccelerationEligibility(): void {
if (this.tx) {
const txHeight = this.tx.status?.block_height || (this.stateService.latestBlockHeight >= 0 ? this.stateService.latestBlockHeight + 1 : null);
this.tx.flags = getTransactionFlags(this.tx, null, null, txHeight, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
} else {
this.eligibleForAcceleration = false;
}
}
onAcceleratorUnavailable(): void {
this.eligibleForAcceleration = false;
this.notAcceleratedOnLoad = false;
}
onAccelerationCompleted(): void {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], { queryParamsHandling: 'merge' });
}
setHasAccelerationDetails(hasDetails: boolean): void {
this.hasAccelerationDetails = hasDetails;
}
get showAccelerationSummary(): boolean {
return (
this.tx
&& !this.tx.status?.confirmed
&& !this.replaced
&& !this.isCached
&& this.acceleratorAvailable
&& this.eligibleForAcceleration
&& this.notAcceleratedOnLoad
);
}
get acceleratorCheckoutLoading(): boolean {
if (this.error || this.showAccelerationSummary) {
return false;
}
return (
this.isLoadingTx
|| this.loadingCachedTx
|| !!(
this.tx
&& !this.tx.status?.confirmed
&& !this.replaced
&& !this.isCached
&& this.acceleratorAvailable
&& this.eligibleForAcceleration
&& this.notAcceleratedOnLoad === null
)
);
}
get acceleratorError(): string {
if (!this.acceleratorAvailable) {
return 'temporarily_unavailable';
}
if (this.tx?.status?.confirmed) {
return 'transaction_confirmed';
}
if (this.isAcceleration) {
return 'acceleration_duplicated';
}
return 'cannot_accelerate_tx';
}
private setupPartnerCode(): void {
this.partnerCodeSubscription?.unsubscribe();
this.partnerCodeSubscription = this.partnerCodeService.partnerCode$.subscribe((partnerCode) => {
this.partnerCode = partnerCode;
});
}
private consumePartnerCodeFragment(fragment: string | null): void {
const fragmentParams = new URLSearchParams(fragment || '');
if (!fragmentParams.has('partnerCode')) {
return;
}
const partnerCode = fragmentParams.get('partnerCode');
if (partnerCode) {
this.partnerCodeService.setFragmentPartnerCode(partnerCode);
}
fragmentParams.delete('partnerCode');
const anchor = Array.from(fragmentParams.entries()).find(([, value]) => value === '')?.[0] || null;
this.router.navigate([], {
relativeTo: this.route,
fragment: this.formatFragment(fragmentParams, anchor),
queryParamsHandling: 'merge',
replaceUrl: true,
});
}
private formatFragment(fragmentParams: URLSearchParams, anchor: string | null = null): string | null {
const params = new URLSearchParams(fragmentParams);
for (const [key, value] of Array.from(params.entries())) {
if (value === '') {
params.delete(key);
}
}
if (anchor) {
params.set(anchor, '');
}
return Array.from(params.entries()).map(([key, value]) => value ? `${key}=${value}` : key).join('&') || null;
}
resetTransaction(): void {
this.error = undefined;
this.tx = null;
this.txChanged$.next(true);
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.loadingCachedTx = false;
this.replaced = false;
this.mempoolPosition = null;
this.accelerationPositions = null;
this.txInBlockIndex = null;
this.isCached = false;
this.isAcceleration = false;
this.notAcceleratedOnLoad = null;
this.eligibleForAcceleration = false;
this.showAccelerationDetails = false;
this.hasAccelerationDetails = false;
this.isAccelerated$.next(this.isAcceleration);
document.body.scrollTo(0, 0);
this.leaveTransaction();
}
leaveTransaction(): void {
this.websocketService.stopTrackingTransaction();
this.stateService.markBlock$.next({});
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth < 850;
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.fetchCpfpSubscription?.unsubscribe();
this.fetchCachedTxSubscription?.unsubscribe();
this.mempoolPositionSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.txConfirmedSubscription?.unsubscribe();
this.txReplacedSubscription?.unsubscribe();
this.urlFragmentSubscription?.unsubscribe();
this.partnerCodeSubscription?.unsubscribe();
this.leaveTransaction();
}
}

View file

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { TransactionComponent } from '@components/transaction/transaction.component';
import { TransactionAccelerateComponent } from '@components/transaction/transaction-accelerate.component';
import { TransactionDetailsComponent } from '@components/transaction/transaction-details/transaction-details.component';
import { SharedModule } from '@app/shared/shared.module';
import { TxBowtieModule } from '@components/tx-bowtie-graph/tx-bowtie.module';
@ -23,6 +24,13 @@ const routes: Routes = [
path: 'preview',
component: TransactionRawComponent,
},
{
path: ':id/accelerate',
component: TransactionAccelerateComponent,
data: {
ogImage: true
}
},
{
path: ':id',
component: TransactionComponent,
@ -53,6 +61,7 @@ export class TransactionRoutingModule { }
],
declarations: [
TransactionComponent,
TransactionAccelerateComponent,
TransactionDetailsComponent,
AccelerateCheckout,
AccelerateFeeGraphComponent,

View file

@ -6,6 +6,7 @@ export const MempoolErrors = {
'internal_server_error': `Something went wrong, please try again later`,
'temporarily_unavailable': `Acceleration temporarily unavailable`,
'acceleration_duplicated': `This transaction has already been accelerated.`,
'transaction_confirmed': `This transaction is already confirmed.`,
'acceleration_outbid': `Your fee delta is too low.`,
'cannot_accelerate_tx': `Cannot accelerate this transaction.`,
'cannot_decode_raw_tx': `Cannot decode this raw transaction.`,