diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 0f37db97e..1e742ae4a 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -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[] = []; diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index dfde33443..8ff5bd3b6 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -72,8 +72,6 @@ -
- @@ -132,6 +130,8 @@ +
+

Inputs & Outputs

@@ -227,11 +227,18 @@ -
- Error loading transaction data. -
- {{ error.error }} + +
+

Transaction not found.

+
Waiting for it to appear in the mempool...
+
+ + +
+

{{ error.error }}

+
+
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index dd904727a..a85b93fc4 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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; + 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 { + 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(); + } } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index fd63d016d..2ae01e064 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -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; } diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 86132621d..e549c7c13 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -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); } }