From 84cda07062b77cef0e2d4771b60b06ea69f62419 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 4 Nov 2025 18:09:42 +0100 Subject: [PATCH 1/4] [accelerator] use /accelerator/accelerations/:txid in transaction component --- .../transaction/transaction.component.ts | 40 +++++++++---------- .../src/app/services/services-api.service.ts | 4 ++ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 12992084b..1a789956b 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -343,12 +343,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe( - switchMap((accelerationHistory: Acceleration[]) => { - if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry + return this.servicesApiService.getAccelerationDataForTxid$(this.txId).pipe( + switchMap((accelerationData: Acceleration) => { + if (this.tx.acceleration && !accelerationData) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry return throwError('retry'); } - return of(accelerationHistory); + return of(accelerationData); }), retry({ count: 3, delay: 2000 }), catchError(() => { @@ -356,25 +356,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }) ); }), - ).subscribe((accelerationHistory) => { - for (const acceleration of accelerationHistory) { - if (acceleration.txid === this.txId) { - if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { - const boostCost = acceleration.boostCost || acceleration.bidBoost; - acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - acceleration.boost = boostCost; - this.tx.acceleratedAt = acceleration.added; - this.accelerationInfo = acceleration; - } - if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') { - this.accelerationCanceled = true; - this.tx.acceleratedAt = acceleration.added; - this.accelerationInfo = acceleration; - } - this.waitingForAccelerationInfo = false; - this.setIsAccelerated(); - } + ).subscribe((acceleration: Acceleration) => { + if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + const boostCost = acceleration.boostCost || acceleration.bidBoost; + acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + acceleration.boost = boostCost; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; } + if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') { + this.accelerationCanceled = true; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } + this.waitingForAccelerationInfo = false; + this.setIsAccelerated(); }); this.miningSubscription = this.fetchMiningInfo$.pipe( diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index d0b04cf79..d85698e42 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -163,6 +163,10 @@ export class ServicesApiServices { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } }); } + getAccelerationDataForTxid$(txid: string) { + return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/${txid}`); + } + getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable { const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => { return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe( From 074d64ac8f5ecba6c2fc5e12834eb6028a3c1392 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 5 Nov 2025 06:23:10 +0000 Subject: [PATCH 2/4] add /accelerator/accelerations/:txid backend route --- .../api/acceleration/acceleration.routes.ts | 14 ++++++++++ .../repositories/AccelerationRepository.ts | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/backend/src/api/acceleration/acceleration.routes.ts b/backend/src/api/acceleration/acceleration.routes.ts index 082d53330..dc2ce697b 100644 --- a/backend/src/api/acceleration/acceleration.routes.ts +++ b/backend/src/api/acceleration/acceleration.routes.ts @@ -11,6 +11,7 @@ class AccelerationRoutes { public initRoutes(app: Application): void { app .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/:txid', this.$getAcceleratorAcceleration.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this)) @@ -23,6 +24,19 @@ class AccelerationRoutes { res.status(200).send(Object.values(accelerations)); } + private async $getAcceleratorAcceleration(req: Request, res: Response): Promise { + if (req.params.txid) { + const acceleration = await AccelerationRepository.$getAccelerationInfoForTxid(req.params.txid); + if (acceleration) { + res.status(200).send(acceleration); + } else { + res.status(404).send('Acceleration not found'); + } + } else { + res.status(400).send('txid is required'); + } + } + private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise { const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null); res.status(200).send(history.map(accel => ({ diff --git a/backend/src/repositories/AccelerationRepository.ts b/backend/src/repositories/AccelerationRepository.ts index 9c1ae2f90..aa39c8929 100644 --- a/backend/src/repositories/AccelerationRepository.ts +++ b/backend/src/repositories/AccelerationRepository.ts @@ -60,6 +60,32 @@ class AccelerationRepository { } } + public async $getAccelerationInfoForTxid(txid: string): Promise { + const [rows] = await DB.query(` + SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations + JOIN pools on pools.unique_id = accelerations.pool + WHERE txid = ? + `, [txid]) as RowDataPacket[][]; + if (rows?.length) { + const row = rows[0]; + return { + txid: row.txid, + height: row.height, + added: row.requested_timestamp || row.block_timestamp, + pool: { + id: row.id, + slug: row.slug, + name: row.name, + }, + effective_vsize: row.effective_vsize, + effective_fee: row.effective_fee, + boost_rate: row.boost_rate, + boost_cost: row.boost_cost, + }; + } + return null; + } + public async $getAccelerationInfo(poolSlug: string | null = null, height: number | null = null, interval: string | null = null): Promise { if (!interval || !['24h', '3d', '1w', '1m'].includes(interval)) { interval = '1m'; From a4ceb3c843fa484b2069ac42c5c6135be2e977fa Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 5 Nov 2025 06:59:03 +0000 Subject: [PATCH 3/4] reduce unnecessary acceleration history requests --- .../block/block-preview.component.ts | 2 +- .../app/components/block/block.component.ts | 2 +- .../transaction/transaction.component.ts | 43 ++++++++++++------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index f5b31e846..29a13308c 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -138,7 +138,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { return of(transactions); }) ), - this.stateService.env.ACCELERATOR === true && block.height > 819500 + this.stateService.env.ACCELERATOR === true && block.height > 819500 && this.stateService.network === '' ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe( catchError(() => { diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index fce33b0dd..c3ad82b8e 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -384,7 +384,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.accelerationsSubscription = this.block$.pipe( switchMap((block) => { - return this.stateService.env.ACCELERATOR === true && block.height > 819500 + return this.stateService.env.ACCELERATOR === true && block.height > 819500 && this.stateService.network === '' ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 1a789956b..fb59e8256 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -16,7 +16,7 @@ import { take } from 'rxjs/operators'; import { Transaction } from '@interfaces/electrs.interface'; -import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject } from 'rxjs'; +import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest, BehaviorSubject, timer } from 'rxjs'; import { StateService } from '@app/services/state.service'; import { CacheService } from '@app/services/cache.service'; import { WebsocketService } from '@app/services/websocket.service'; @@ -343,28 +343,41 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAccelerationDataForTxid$(this.txId).pipe( - switchMap((accelerationData: Acceleration) => { - if (this.tx.acceleration && !accelerationData) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry - return throwError('retry'); - } - return of(accelerationData); - }), - retry({ count: 3, delay: 2000 }), - catchError(() => { - return of([]); - }) - ); + if (this.stateService.network === '' && this.stateService.env.ACCELERATOR && blockHeight >= 819500 ) { + return this.servicesApiService.getAccelerationDataForTxid$(this.txId).pipe( + switchMap((accelerationData: Acceleration) => { + if (this.tx.acceleration && !accelerationData) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry + return throwError(() => 'retry'); + } + return of(accelerationData); + }), + retry({ + count: 3, + delay: (error) => { + if (error === 'retry') { + return timer(2000); + } + return throwError(() => error); + } + }), + catchError(() => { + return of(null); + }) + ); + } else { + return of(null); + } }), + filter((acceleration: Acceleration) => !!acceleration), ).subscribe((acceleration: Acceleration) => { - if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { const boostCost = acceleration.boostCost || acceleration.bidBoost; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.boost = boostCost; this.tx.acceleratedAt = acceleration.added; this.accelerationInfo = acceleration; } - if (acceleration.status === 'failed' || acceleration.status === 'failed_provisional') { + if (acceleration.txid === this.txId && (acceleration.status === 'failed' || acceleration.status === 'failed_provisional')) { this.accelerationCanceled = true; this.tx.acceleratedAt = acceleration.added; this.accelerationInfo = acceleration; From 03023b106c4c40ef46b0ff58d74997703c02473e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 5 Nov 2025 07:00:09 +0000 Subject: [PATCH 4/4] update tracker acceleration history pipeline --- .../components/tracker/tracker.component.ts | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index f78b7a2a9..185fcddc2 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -10,10 +10,11 @@ import { mergeMap, tap, map, - startWith + startWith, + retry } from 'rxjs/operators'; import { Transaction } from '@interfaces/electrs.interface'; -import { of, merge, Subscription, Observable, Subject, throwError, combineLatest, BehaviorSubject } from 'rxjs'; +import { of, merge, Subscription, Observable, Subject, throwError, combineLatest, BehaviorSubject, timer } from 'rxjs'; import { StateService } from '@app/services/state.service'; import { CacheService } from '@app/services/cache.service'; import { WebsocketService } from '@app/services/websocket.service'; @@ -101,7 +102,7 @@ export class TrackerComponent implements OnInit, OnDestroy { fetchCpfp$ = new Subject(); fetchRbfHistory$ = new Subject(); fetchCachedTx$ = new Subject(); - fetchAcceleration$ = new Subject(); + fetchAcceleration$ = new Subject(); fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>(); txChanged$ = new BehaviorSubject(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself) isAccelerated$ = new BehaviorSubject(false); // refactor this to make isAccelerated an observable itself @@ -284,24 +285,48 @@ export class TrackerComponent implements OnInit, OnDestroy { filter(() => this.stateService.env.ACCELERATOR === true), tap(() => { this.accelerationInfo = null; + this.setIsAccelerated(); }), - switchMap((blockHash: string) => { - return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId); - }), - catchError(() => { - return of(null); - }) - ).subscribe((accelerationHistory) => { - for (const acceleration of accelerationHistory) { - if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { - const boostCost = acceleration.boostCost || acceleration.bidBoost; - acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - acceleration.boost = boostCost; - - this.accelerationInfo = acceleration; - this.setIsAccelerated(); + switchMap((blockHeight: number) => { + if (this.stateService.network === '' && this.stateService.env.ACCELERATOR && blockHeight >= 819500 ) { + return this.servicesApiService.getAccelerationDataForTxid$(this.txId).pipe( + switchMap((accelerationData: Acceleration) => { + if (this.tx.acceleration && !accelerationData) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry + return throwError(() => 'retry'); + } + return of(accelerationData); + }), + retry({ + count: 3, + delay: (error) => { + if (error === 'retry') { + return timer(2000); + } + return throwError(() => error); // Don't retry, just rethrow + } + }), + catchError(() => { + return of(null); + }) + ); + } else { + return of(null); } + }), + filter((acceleration: Acceleration) => !!acceleration), + ).subscribe((acceleration: Acceleration) => { + if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + const boostCost = acceleration.boostCost || acceleration.bidBoost; + acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + acceleration.boost = boostCost; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; } + if (acceleration.txid === this.txId && (acceleration.status === 'failed' || acceleration.status === 'failed_provisional')) { + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } + this.setIsAccelerated(); }); this.miningSubscription = this.fetchMiningInfo$.pipe( @@ -476,7 +501,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } else { this.trackerStage = 'confirmed'; this.loadingPosition = false; - this.fetchAcceleration$.next(tx.status.block_hash); + this.fetchAcceleration$.next(tx.status.block_height); this.fetchMiningInfo$.next({ hash: tx.status.block_hash, height: tx.status.block_height, txid: tx.txid }); this.transactionTime = 0; } @@ -540,7 +565,7 @@ export class TrackerComponent implements OnInit, OnDestroy { } else { this.audioService.playSound('magic'); } - this.fetchAcceleration$.next(block.id); + this.fetchAcceleration$.next(block.height); this.fetchMiningInfo$.next({ hash: block.id, height: block.height, txid: this.tx.txid }); } });