Transaction tracking revamped.

Blockchain block arrow.
This commit is contained in:
Simon Lindh 2020-02-19 23:50:23 +07:00 committed by wiz
parent 34645908e9
commit f3cfa038d3
No known key found for this signature in database
GPG Key ID: A394E332255A6173
15 changed files with 232 additions and 145 deletions

View File

@ -90,8 +90,12 @@ class Server {
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1; client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
} }
if (parsedMessage && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) { if (parsedMessage && parsedMessage['track-tx']) {
client['txId'] = parsedMessage.txId; if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx'];
} else {
client['track-tx'] = null;
}
} }
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
@ -139,8 +143,8 @@ class Server {
return; return;
} }
if (client['txId'] && txIds.indexOf(client['txId']) > -1) { if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
client['txId'] = null; client['track-tx'] = null;
client.send(JSON.stringify({ client.send(JSON.stringify({
'block': block, 'block': block,
'txConfirmed': true, 'txConfirmed': true,

View File

@ -5,6 +5,7 @@
.qr-wrapper { .qr-wrapper {
background-color: #FFF; background-color: #FFF;
padding: 10px; padding: 10px;
padding-bottom: 5px;
display: inline-block; display: inline-block;
margin-right: 25px; margin-right: 25px;
} }

View File

@ -1,134 +1,136 @@
<div class="container"> <div class="container">
<app-blockchain position="top"></app-blockchain> <app-blockchain position="top" [markHeight]="blockHeight"></app-blockchain>
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/', blockHash]">#{{ blockHeight }}</a></ng-template></h1>
<ng-template [ngIf]="!isLoadingBlock && !error">
<br>
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Timestamp</td>
<td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} <i>(<app-time-since [time]="block.timestamp"></app-time-since> ago)</i></td>
</tr>
<tr>
<td>Number of transactions</td>
<td>{{ block.tx_count }}</td>
</tr>
<tr>
<td>Size</td>
<td>{{ block.size | bytes: 2 }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ block.weight | wuBytes: 2 }}</td>
</tr>
<tr>
<td>Status</td>
<td><button *ngIf="latestBlock" class="btn btn-sm btn-success">{{ (latestBlock.height - block.height + 1) }} confirmation{{ (latestBlock.height - block.height + 1) === 1 ? '' : 's' }}</button></td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Hash</td>
<td><a [routerLink]="['/block/', block.id]" title="{{ block.id }}" >{{ block.id | shortenString : 32 }}</a></td>
</tr>
<tr>
<td>Previous Block</td>
<td><a [routerLink]="['/block/', block.previousblockhash]" [state]="{ data: { blockHeight: blockHeight - 1 } }" title="{{ block.previousblockhash }}">{{ block.previousblockhash | shortenString : 32 }}</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ block.tx_count }} transactions</h2>
<br>
<app-transactions-list [transactions]="transactions"></app-transactions-list>
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">
<div class="spinner-border"></div>
<br><br>
</ng-template>
<button *ngIf="transactions?.length && transactions?.length !== block.tx_count" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
</div>
</ng-template>
<ng-template [ngIf]="isLoadingBlock && !error">
<br>
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="text-center">
<div class="spinner-border"></div>
<br><br>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
Error loading block data.
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
<div class="title-block">
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/', blockHash]">#{{ blockHeight }}</a></ng-template></h1>
</div> </div>
<br> <ng-template [ngIf]="!isLoadingBlock && !error">
<br>
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Timestamp</td>
<td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} <i>(<app-time-since [time]="block.timestamp"></app-time-since> ago)</i></td>
</tr>
<tr>
<td>Number of transactions</td>
<td>{{ block.tx_count }}</td>
</tr>
<tr>
<td>Size</td>
<td>{{ block.size | bytes: 2 }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ block.weight | wuBytes: 2 }}</td>
</tr>
<tr>
<td>Status</td>
<td><button *ngIf="latestBlock" class="btn btn-sm btn-success">{{ (latestBlock.height - block.height + 1) }} confirmation{{ (latestBlock.height - block.height + 1) === 1 ? '' : 's' }}</button></td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Hash</td>
<td><a [routerLink]="['/block/', block.id]" title="{{ block.id }}" >{{ block.id | shortenString : 32 }}</a></td>
</tr>
<tr>
<td>Previous Block</td>
<td><a [routerLink]="['/block/', block.previousblockhash]" [state]="{ data: { blockHeight: blockHeight - 1 } }" title="{{ block.previousblockhash }}">{{ block.previousblockhash | shortenString : 32 }}</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ block.tx_count }} transactions</h2>
<br>
<app-transactions-list [transactions]="transactions"></app-transactions-list>
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">
<div class="spinner-border"></div>
<br><br>
</ng-template>
<button *ngIf="transactions?.length && transactions?.length !== block.tx_count" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
</div>
</ng-template>
<ng-template [ngIf]="isLoadingBlock && !error">
<br>
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="text-center">
<div class="spinner-border"></div>
<br><br>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
Error loading block data.
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
</div>
<br>

View File

@ -0,0 +1,11 @@
.title-block {
color: #FFF;
padding-left: 10px;
padding-top: 13px;
padding-bottom: 3px;
border-top: 5px solid #FFF;
}
.title-block > h1 {
margin: 0;
}

View File

@ -18,4 +18,5 @@
</div> </div>
</div> </div>
</div> </div>
<div [hidden]="!arrowVisible" id="arrow-up" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
</div> </div>

View File

@ -101,3 +101,15 @@
z-index: 100; z-index: 100;
position: relative; position: relative;
} }
#arrow-up {
position: relative;
left: 30px;
top: 140px;
transition: 1s;
width: 0;
height: 0;
border-left: 35px solid transparent;
border-right: 35px solid transparent;
border-bottom: 35px solid #FFF;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, Input, OnChanges } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Block } from 'src/app/interfaces/electrs.interface'; import { Block } from 'src/app/interfaces/electrs.interface';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
@ -8,12 +8,18 @@ import { StateService } from 'src/app/services/state.service';
templateUrl: './blockchain-blocks.component.html', templateUrl: './blockchain-blocks.component.html',
styleUrls: ['./blockchain-blocks.component.scss'] styleUrls: ['./blockchain-blocks.component.scss']
}) })
export class BlockchainBlocksComponent implements OnInit, OnDestroy { export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() markHeight = 0;
blocks: Block[] = []; blocks: Block[] = [];
blocksSubscription: Subscription; blocksSubscription: Subscription;
interval: any; interval: any;
trigger = 0; trigger = 0;
arrowVisible = false;
arrowLeftPx = 30;
constructor( constructor(
private stateService: StateService, private stateService: StateService,
) { } ) { }
@ -26,16 +32,34 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
} }
this.blocks.unshift(block); this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 8); this.blocks = this.blocks.slice(0, 8);
this.moveArrowToPosition();
}); });
this.interval = setInterval(() => this.trigger++, 10 * 1000); this.interval = setInterval(() => this.trigger++, 10 * 1000);
} }
ngOnChanges() {
this.moveArrowToPosition();
}
ngOnDestroy() { ngOnDestroy() {
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
clearInterval(this.interval); clearInterval(this.interval);
} }
moveArrowToPosition() {
if (!this.markHeight) {
this.arrowVisible = false;
return;
}
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
if (blockindex !== -1) {
this.arrowVisible = true;
this.arrowLeftPx = blockindex * 150 + 30;
}
}
trackByBlocksFn(index: number, item: Block) { trackByBlocksFn(index: number, item: Block) {
return item.height; return item.height;
} }

View File

@ -4,9 +4,9 @@
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="text-center" class="blockchain-wrapper"> <div class="text-center" class="blockchain-wrapper">
<div class="position-container" [ngStyle]="{'top': position === 'top' ? '100px' : 'calc(50% - 60px)'}"> <div class="position-container" [ngStyle]="{'top': position === 'top' ? '75px' : 'calc(50% - 60px)'}">
<app-mempool-blocks></app-mempool-blocks> <app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks></app-blockchain-blocks> <app-blockchain-blocks [markHeight]="markHeight"></app-blockchain-blocks>
<div id="divider" *ngIf="!isLoading"></div> <div id="divider" *ngIf="!isLoading"></div>
</div> </div>

View File

@ -10,6 +10,7 @@ import { StateService } from 'src/app/services/state.service';
}) })
export class BlockchainComponent implements OnInit, OnDestroy { export class BlockchainComponent implements OnInit, OnDestroy {
@Input() position: 'middle' | 'top' = 'middle'; @Input() position: 'middle' | 'top' = 'middle';
@Input() markHeight: number;
txTrackingSubscription: Subscription; txTrackingSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;

View File

@ -15,6 +15,8 @@ export class SearchFormComponent implements OnInit {
searchBoxPlaceholderText = 'Transaction, address, block hash...'; searchBoxPlaceholderText = 'Transaction, address, block hash...';
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/; regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^[a-fA-F0-9]{64}$/;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@ -32,8 +34,12 @@ export class SearchFormComponent implements OnInit {
if (searchText) { if (searchText) {
if (this.regexAddress.test(searchText)) { if (this.regexAddress.test(searchText)) {
this.router.navigate(['/address/', searchText]); this.router.navigate(['/address/', searchText]);
} else { } else if (this.regexBlockhash.test(searchText)) {
this.router.navigate(['/block/', searchText]);
} else if (this.regexTransaction.test(searchText)) {
this.router.navigate(['/tx/', searchText]); this.router.navigate(['/tx/', searchText]);
} else {
return;
} }
this.searchForm.setValue({ this.searchForm.setValue({
searchText: '', searchText: '',

View File

@ -1,10 +1,14 @@
<div class="container"> <div class="container">
<app-blockchain position="top"></app-blockchain> <app-blockchain position="top" [markHeight]="tx?.status?.block_height"></app-blockchain>
<div class="clearfix"></div> <div class="title-block">
<h1 style="float: left;">Transaction</h1>
<a [routerLink]="['/tx/', txId]" style="line-height: 55px; margin-left: 10px;">{{ txId }}</a>
<app-clipboard [text]="txId"></app-clipboard>
</div>
<h1>Transaction</h1> <br>
<ng-template [ngIf]="!isLoadingTx && !error"> <ng-template [ngIf]="!isLoadingTx && !error">
@ -51,19 +55,14 @@
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>
<td>Transaction</td> <td>Status</td>
<td>
<a [routerLink]="['/tx/', txId]">{{ txId | shortenString }}</a>
<app-clipboard [text]="txId"></app-clipboard>
</td>
<td class="adjust-btn-padding"> <td class="adjust-btn-padding">
<button type="button" class="btn btn-sm btn-danger">Unconfirmed</button> <button type="button" class="btn btn-sm btn-danger">Unconfirmed</button>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Fees</td> <td>Fees</td>
<td>{{ tx.fee | number }} sats <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>)</span></td> <td>{{ tx.fee | number }} sats <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>)</span> {{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -10,3 +10,15 @@
width: 40px; width: 40px;
} }
.title-block {
color: #FFF;
padding-left: 10px;
padding-top: 13px;
padding-bottom: 3px;
border-top: 5px solid #FFF;
}
.title-block > h1 {
margin: 0;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
@ -12,7 +12,7 @@ import { WebsocketService } from '../../services/websocket.service';
templateUrl: './transaction.component.html', templateUrl: './transaction.component.html',
styleUrls: ['./transaction.component.scss'] styleUrls: ['./transaction.component.scss']
}) })
export class TransactionComponent implements OnInit { export class TransactionComponent implements OnInit, OnDestroy {
tx: Transaction; tx: Transaction;
txId: string; txId: string;
isLoadingTx = true; isLoadingTx = true;
@ -51,7 +51,7 @@ export class TransactionComponent implements OnInit {
window.scrollTo(0, 0); window.scrollTo(0, 0);
if (!tx.status.confirmed) { if (!tx.status.confirmed) {
this.websocketService.startTrackTx(tx.txid); this.websocketService.startTrackTransaction(tx.txid);
} }
}, },
(error) => { (error) => {
@ -75,4 +75,8 @@ export class TransactionComponent implements OnInit {
}; };
}); });
} }
ngOnDestroy() {
this.websocketService.startTrackTransaction('stop');
}
} }

View File

@ -4,13 +4,14 @@ export interface WebsocketResponse {
block?: Block; block?: Block;
blocks?: Block[]; blocks?: Block[];
conversions?: any; conversions?: any;
txId?: string;
txConfirmed?: boolean; txConfirmed?: boolean;
historicalDate?: string; historicalDate?: string;
mempoolInfo?: MempoolInfo; mempoolInfo?: MempoolInfo;
vBytesPerSecond?: number; vBytesPerSecond?: number;
action?: string; action?: string;
data?: string[]; data?: string[];
'track-tx'?: string;
'track-address'?: string;
} }
export interface MempoolBlock { export interface MempoolBlock {

View File

@ -16,6 +16,7 @@ export class WebsocketService {
private goneOffline = false; private goneOffline = false;
private lastWant: string[] | null = null; private lastWant: string[] | null = null;
private trackingTxId: string | null = null; private trackingTxId: string | null = null;
private trackingAddress: string | null = null;
constructor( constructor(
private stateService: StateService, private stateService: StateService,
@ -86,7 +87,10 @@ export class WebsocketService {
this.want(this.lastWant); this.want(this.lastWant);
} }
if (this.trackingTxId) { if (this.trackingTxId) {
this.startTrackTx(this.trackingTxId); this.startTrackTransaction(this.trackingTxId);
}
if (this.trackingAddress) {
this.startTrackTransaction(this.trackingAddress);
} }
this.stateService.isOffline$.next(false); this.stateService.isOffline$.next(false);
} }
@ -99,11 +103,16 @@ export class WebsocketService {
}); });
} }
startTrackTx(txId: string) { startTrackTransaction(txId: string) {
this.websocketSubject.next({ txId }); this.websocketSubject.next({ 'track-tx': txId });
this.trackingTxId = txId; this.trackingTxId = txId;
} }
startTrackAddress(address: string) {
this.websocketSubject.next({ 'track-address': address });
this.trackingAddress = address;
}
fetchStatistics(historicalDate: string) { fetchStatistics(historicalDate: string) {
this.websocketSubject.next({ historicalDate }); this.websocketSubject.next({ historicalDate });
} }