Wait for an push transactions that hasn't yet appeared in the mempool

This commit is contained in:
softsimon 2020-04-13 01:26:53 +07:00
parent c5796a8062
commit e2671df4be
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
5 changed files with 90 additions and 33 deletions

View File

@ -133,6 +133,13 @@ class WebsocketHandler {
response['mempool-blocks'] = mBlocks;
}
if (client['track-tx']) {
const tx = newTransactions.find((t) => t.txid === client['track-tx']);
if (tx) {
response['tx'] = tx;
}
}
// Send all new incoming transactions related to tracked address
if (client['track-address']) {
const foundTransactions: TransactionExtended[] = [];

View File

@ -72,8 +72,6 @@
</div>
</div>
<br>
</ng-template>
<ng-template #unconfirmedTemplate>
@ -132,6 +130,8 @@
</div>
</ng-template>
<br>
<h2>Inputs & Outputs</h2>
<app-transactions-list [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>
@ -227,11 +227,18 @@
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
Error loading transaction data.
<br>
<i>{{ error.error }}</i>
<div class="text-center" *ngIf="waitingForTransaction">
<h3>Transaction not found.</h3>
<h5>Waiting for it to appear in the mempool...</h5>
<div class="spinner-border text-light mt-2"></div>
</div>
<ng-template #errorTemplate>
<div class="text-center">
<h3>{{ error.error }}</h3>
</div>
</ng-template>
</ng-template>
</div>

View File

@ -1,9 +1,9 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, filter, take } from 'rxjs/operators';
import { switchMap, filter, take, catchError, mergeMap, flatMap, mergeAll, tap, map } from 'rxjs/operators';
import { Transaction, Block } from '../../interfaces/electrs.interface';
import { of, merge, Subscription } from 'rxjs';
import { of, merge, Subscription, Observable, scheduled } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from 'src/app/services/audio.service';
@ -26,6 +26,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
txInBlockIndex: number;
isLoadingTx = true;
error: any = undefined;
waitingForTransaction = false;
latestBlock: Block;
transactionTime = -1;
subscription: Subscription;
@ -47,35 +48,47 @@ export class TransactionComponent implements OnInit, OnDestroy {
switchMap((params: ParamMap) => {
this.txId = params.get('id') || '';
this.seoService.setTitle('Transaction: ' + this.txId, true);
this.error = undefined;
this.feeRating = undefined;
this.isLoadingTx = true;
this.transactionTime = -1;
document.body.scrollTo(0, 0);
this.leaveTransaction();
this.resetTransaction();
return merge(
of(true),
this.stateService.connectionState$
.pipe(filter((state) => state === 2 && this.tx && !this.tx.status.confirmed) ),
)
.pipe(
switchMap(() => {
if (history.state.data) {
return of(history.state.data);
}
return this.electrsApiService.getTransaction$(this.txId);
})
this.stateService.connectionState$.pipe(
filter((state) => state === 2 && this.tx && !this.tx.status.confirmed)
),
);
}),
flatMap(() => {
let transactionObservable$: Observable<Transaction>;
if (history.state.data) {
transactionObservable$ = of(history.state.data);
} else {
transactionObservable$ = this.electrsApiService.getTransaction$(this.txId).pipe(
catchError(this.handleLoadElectrsTransactionError.bind(this))
);
}
return merge(
transactionObservable$,
this.stateService.mempoolTransactions$
);
})
)
.subscribe((tx: Transaction) => {
if (!tx) {
return;
}
this.tx = tx;
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.setMempoolBlocksSubscription();
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
this.getTransactionTime();
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
}
} else {
this.findBlockAndSetFeeRating();
}
@ -107,6 +120,16 @@ export class TransactionComponent implements OnInit, OnDestroy {
});
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
this.websocketService.startTrackTransaction(this.txId);
this.waitingForTransaction = true;
}
this.error = error;
this.isLoadingTx = false;
return of(false);
}
setMempoolBlocksSubscription() {
this.stateService.mempoolBlocks$
.subscribe((mempoolBlocks) => {
@ -161,8 +184,14 @@ export class TransactionComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
resetTransaction() {
this.error = undefined;
this.tx = null;
this.feeRating = undefined;
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.transactionTime = -1;
document.body.scrollTo(0, 0);
this.leaveTransaction();
}
@ -170,4 +199,9 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.websocketService.stopTrackingTransaction();
this.stateService.markBlock$.next({});
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.leaveTransaction();
}
}

View File

@ -1,4 +1,4 @@
import { Block } from './electrs.interface';
import { Block, Transaction } from './electrs.interface';
export interface WebsocketResponse {
block?: Block;
@ -10,6 +10,7 @@ export interface WebsocketResponse {
vBytesPerSecond?: number;
action?: string;
data?: string[];
tx?: Transaction;
'track-tx'?: string;
'track-address'?: string;
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { WebsocketResponse, MempoolBlock } from '../interfaces/websocket.interface';
import { WebsocketResponse } from '../interfaces/websocket.interface';
import { StateService } from './state.service';
import { Block, Transaction } from '../interfaces/electrs.interface';
import { Subscription } from 'rxjs';
@ -8,6 +8,10 @@ import { Subscription } from 'rxjs';
const WEB_SOCKET_PROTOCOL = (document.location.protocol === 'https:') ? 'wss:' : 'ws:';
const WEB_SOCKET_URL = WEB_SOCKET_PROTOCOL + '//' + document.location.hostname + ':' + document.location.port + '/ws';
const OFFLINE_RETRY_AFTER_MS = 10000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
const EXPECT_PING_RESPONSE_AFTER_MS = 1000;
@Injectable({
providedIn: 'root'
})
@ -44,6 +48,10 @@ export class WebsocketService {
});
}
if (response.tx) {
this.stateService.mempoolTransactions$.next(response.tx);
}
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.latestBlockHeight = response.block.height;
@ -115,7 +123,7 @@ export class WebsocketService {
},
(err: Error) => {
console.log(err);
console.log('WebSocket error, trying to reconnect in 10 seconds');
console.log(`WebSocket error, trying to reconnect in ${OFFLINE_RETRY_AFTER_MS} seconds`);
this.goOffline();
});
}
@ -155,7 +163,7 @@ export class WebsocketService {
this.stateService.connectionState$.next(0);
window.setTimeout(() => {
this.startSubscription(true);
}, 10000);
}, OFFLINE_RETRY_AFTER_MS);
}
startOnlineCheck() {
@ -171,7 +179,7 @@ export class WebsocketService {
this.subscription.unsubscribe();
this.goOffline();
}
}, 1000);
}, 30000);
}, EXPECT_PING_RESPONSE_AFTER_MS);
}, OFFLINE_PING_CHECK_AFTER_MS);
}
}