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'; 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/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 }); } }); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 12992084b..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,38 +343,47 @@ 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 throwError('retry'); - } - return of(accelerationHistory); - }), - retry({ count: 3, delay: 2000 }), - catchError(() => { - return of([]); - }) - ); - }), - ).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(); + 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.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.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(