mirror of
https://github.com/mempool/mempool.git
synced 2025-03-03 17:47:01 +01:00
Adding ETA, confirmed time, and other ui improvements to transaction page.
This commit is contained in:
parent
245af5fa8f
commit
93c5f0bd84
10 changed files with 207 additions and 84 deletions
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
44
frontend/src/app/components/timespan/timespan.component.ts
Normal file
44
frontend/src/app/components/timespan/timespan.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,46 +17,60 @@
|
|||
</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">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Included in block</td>
|
||||
<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>
|
||||
<div class="md-inline"> at {{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</div>
|
||||
<div class="md-inline"> <i>(<app-time-since [time]="tx.status.block_time"></app-time-since> ago)</i></div>
|
||||
<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.2-2' }} sats/vB
|
||||
{{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sats/vB
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-template>
|
||||
|
@ -64,7 +78,8 @@
|
|||
<ng-template #unconfirmedTemplate>
|
||||
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<ng-template [ngIf]="transactionTime !== 0">
|
||||
|
@ -75,21 +90,40 @@
|
|||
<ng-template #firstSeenTmpl>
|
||||
<tr>
|
||||
<td>First seen</td>
|
||||
<td><i><app-time-since [time]="transactionTime"></app-time-since> ago</i></td>
|
||||
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since> ago</i></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>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<br>
|
||||
|
@ -119,8 +153,14 @@
|
|||
<ng-template [ngIf]="isLoadingTx && !error">
|
||||
|
||||
<div class="box">
|
||||
<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>
|
||||
|
@ -128,6 +168,22 @@
|
|||
</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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -324,6 +324,10 @@ tr {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.lg-inline {
|
||||
display: inline-block;
|
||||
|
|
Loading…
Add table
Reference in a new issue