Merge branch 'master' into knorrium/db_tests

This commit is contained in:
Felipe Knorr Kuhn 2025-11-06 10:33:40 -08:00
commit 165eae6eba
No known key found for this signature in database
GPG key ID: 79619B52BB097C1A
7 changed files with 131 additions and 53 deletions

View file

@ -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<void> {
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<void> {
const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null);
res.status(200).send(history.map(accel => ({

View file

@ -60,6 +60,32 @@ class AccelerationRepository {
}
}
public async $getAccelerationInfoForTxid(txid: string): Promise<PublicAcceleration | null> {
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<PublicAcceleration[]> {
if (!interval || !['24h', '3d', '1w', '1m'].includes(interval)) {
interval = '1m';

View file

@ -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(() => {

View file

@ -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([]);

View file

@ -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<string>();
fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>();
fetchAcceleration$ = new Subject<string>();
fetchAcceleration$ = new Subject<number>();
fetchMiningInfo$ = new Subject<{ hash: string, height: number, txid: string }>();
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
isAccelerated$ = new BehaviorSubject<boolean>(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 });
}
});

View file

@ -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(

View file

@ -163,6 +163,10 @@ export class ServicesApiServices {
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } });
}
getAccelerationDataForTxid$(txid: string) {
return this.httpClient.get<Acceleration>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/${txid}`);
}
getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable<Acceleration[]> {
const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => {
return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe(