Adding ETA, confirmed time, and other ui improvements to transaction page.

This commit is contained in:
softsimon 2020-03-23 04:07:31 +07:00
parent 245af5fa8f
commit 93c5f0bd84
No known key found for this signature in database
GPG key ID: 488D7DCFB5A430D7
10 changed files with 207 additions and 84 deletions

View file

@ -42,6 +42,7 @@ import { AudioService } from './services/audio.service';
import { FiatComponent } from './fiat/fiat.component';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
import { TimespanComponent } from './components/timespan/timespan.component';
@NgModule({
declarations: [
@ -66,6 +67,7 @@ import { FeeDistributionGraphComponent } from './components/fee-distribution-gra
SearchFormComponent,
LatestBlocksComponent,
TimeSinceComponent,
TimespanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
QrcodeComponent,

View file

@ -18,7 +18,7 @@
<td>
{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i>(<app-time-since [time]="block.timestamp"></app-time-since> ago)</i>
<i>(<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago)</i>
</div>
</td>
</tr>

View file

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

View file

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

View file

@ -0,0 +1,44 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-timespan',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimespanComponent implements OnChanges {
@Input() time: number;
text: string;
constructor() { }
ngOnChanges() {
const seconds = this.time;
if (seconds < 60) {
return '< 1 minute';
}
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
let counter;
for (const i in intervals) {
if (intervals.hasOwnProperty(i)) {
counter = Math.floor(seconds / intervals[i]);
if (counter > 0) {
if (counter === 1) {
this.text = counter + ' ' + i; // singular (1 day ago)
break;
} else {
this.text = counter + ' ' + i + 's'; // plural (2 days ago)
break;
}
}
}
}
}
}

View file

@ -17,44 +17,58 @@
</a>
<app-clipboard [text]="txId"></app-clipboard>
</div>
<div class="clearfix"></div>
</div>
<br>
<ng-template [ngIf]="!isLoadingTx && !error">
<ng-template [ngIf]="tx.status.confirmed" [ngIfElse]="unconfirmedTemplate">
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Included in block</td>
<td>
<a [routerLink]="['/block/', tx.status.block_hash]" [state]="{ data: { blockHeight: tx.status.block_height } }">{{ tx.status.block_height }}</a>
<div class="md-inline">&nbsp;at {{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</div>
<div class="md-inline">&nbsp;<i>(<app-time-since [time]="tx.status.block_time"></app-time-since> ago)</i></div>
</td>
</tr>
<ng-template [ngIf]="tx.fee">
<tr>
<td>Fee per vByte</td>
<td>
{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sats/vB
&nbsp;
<span *ngIf="feeRating === 1" class="badge badge-success">Optimal</span>
<span *ngIf="feeRating === 2" class="badge badge-warning" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
<span *ngIf="feeRating === 3" class="badge badge-danger" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
</td>
</tr>
<tr>
<td>Fee</td>
<td>{{ tx.fee | number }} sats (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
</ng-template>
</tbody>
</table>
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Included in block</td>
<td>
<a [routerLink]="['/block/', tx.status.block_hash]" [state]="{ data: { blockHeight: tx.status.block_height } }">{{ tx.status.block_height }}</a>
<i> (<app-time-since [time]="tx.status.block_time" [fastRender]="true"></app-time-since> ago)</i>
</td>
</tr>
<ng-template [ngIf]="transactionTime > 0">
<tr>
<td>Confirmed</td>
<td>After <app-timespan [time]="tx.status.block_time - transactionTime"></app-timespan></td>
</tr>
</ng-template>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="tx.fee">
<tr>
<td class="td-width">Fee</td>
<td>{{ tx.fee | number }} sats (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td>Fee per vByte</td>
<td>
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sats/vB
&nbsp;
<span *ngIf="feeRating === 1" class="badge badge-success">Optimal</span>
<span *ngIf="feeRating === 2" class="badge badge-warning" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
<span *ngIf="feeRating === 3" class="badge badge-danger" title="Only ~{{ medianFeeNeeded }} sat/vB was needed to get into this block">Overpaid {{ overpaidTimes }}x</span>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</div>
</div>
<br>
@ -64,31 +78,51 @@
<ng-template #unconfirmedTemplate>
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="transactionTime !== 0">
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<ng-template #firstSeenTmpl>
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<ng-template [ngIf]="transactionTime !== 0">
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<ng-template #firstSeenTmpl>
<tr>
<td>First seen</td>
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since> ago</i></td>
</tr>
</ng-template>
</ng-template>
<tr>
<td>First seen</td>
<td><i><app-time-since [time]="transactionTime"></app-time-since> ago</i></td>
<td class="td-width">ETA</td>
<td>
<ng-template [ngIf]="txInBlockIndex === undefined" [ngIfElse]="estimationTmpl">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #estimationTmpl>
~{{ 10 * txInBlockIndex + 10 }} minutes <i>({{ txInBlockIndex + 1 }} block{{ txInBlockIndex > 0 ? 's' : '' }})</i>
</ng-template>
</td>
</tr>
</ng-template>
</ng-template>
<tr>
<td>Fees</td>
<td>{{ tx.fee | number }} sats (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td>Fees per vByte</td>
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sats/vB</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Fee</td>
<td>{{ tx.fee | number }} sats (<app-fiat [value]="tx.fee"></app-fiat>)</td>
</tr>
<tr>
<td>Fee per vByte</td>
<td>{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sats/vB</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-template>
@ -119,14 +153,36 @@
<ng-template [ngIf]="isLoadingTx && !error">
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>

View file

@ -4,19 +4,22 @@
.title-block {
color: #FFF;
padding-left: 10px;
padding-top: 20px;
padding-bottom: 3px;
border-top: 5px solid #FFF;
}
.title-block > h1 {
margin: 0;
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
}
.td-width {
width: 175px;
}
@media (max-width: 767.98px) {
.td-width {
width: 150px;
}
}

View file

@ -1,13 +1,14 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, filter } from 'rxjs/operators';
import { switchMap, filter, take } from 'rxjs/operators';
import { Transaction, Block } from '../../interfaces/electrs.interface';
import { of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
@Component({
selector: 'app-transaction',
@ -20,13 +21,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
feeRating: number;
overpaidTimes: number;
medianFeeNeeded: number;
txInBlockIndex: number;
isLoadingTx = true;
error: any = undefined;
latestBlock: Block;
transactionTime = -1;
rightPosition = 0;
blockDepth = 0;
constructor(
private route: ActivatedRoute,
@ -56,6 +57,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
.subscribe((tx: Transaction) => {
this.tx = tx;
this.isLoadingTx = false;
this.setMempoolBlocksSubscription();
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
@ -91,6 +93,25 @@ export class TransactionComponent implements OnInit, OnDestroy {
});
}
setMempoolBlocksSubscription() {
this.stateService.mempoolBlocks$
.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
const txFeePerVSize = this.tx.fee / (this.tx.weight / 4);
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (txFeePerVSize < block.feeRange[i + 1] && txFeePerVSize >= block.feeRange[i]) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
}
}
}
});
}
getTransactionTime() {
this.apiService.getTransactionTimes$([this.tx.txid])
.subscribe((transactionTimes) => {
@ -100,7 +121,10 @@ export class TransactionComponent implements OnInit, OnDestroy {
findBlockAndSetFeeRating() {
this.stateService.blocks$
.pipe(filter((block) => block.height === this.tx.status.block_height))
.pipe(
filter((block) => block.height === this.tx.status.block_height),
take(1)
)
.subscribe((block) => {
const feePervByte = this.tx.fee / (this.tx.weight / 4);
this.medianFeeNeeded = block.feeRange[Math.round(block.feeRange.length * 0.5)];

View file

@ -7,7 +7,7 @@
<div class="float-right">
<ng-template [ngIf]="tx.status.confirmed">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time-since [time]="tx.firstSeen"></app-time-since> ago</i>
<i><app-time-since [time]="tx.firstSeen" [fastRender]="true"></app-time-since> ago</i>
</ng-template>
</div>
</div>

View file

@ -324,6 +324,10 @@ tr {
white-space: nowrap;
}
h1, h2, h3 {
margin-bottom: 15px;
}
@media (min-width: 992px) {
.lg-inline {
display: inline-block;