Block view.

This commit is contained in:
Simon Lindh
2019-11-12 16:39:59 +08:00
parent 2dbfa323fa
commit 4de8384708
24 changed files with 455 additions and 126 deletions

View File

@@ -1,5 +1,8 @@
<div class="modal-header">
<h4 class="modal-title">Fee distribution for block <a href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a></h4>
<h4 class="modal-title">Fee distribution for block
<a *ngIf="!isEsploraEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
<a *ngIf="isEsploraEnabled" (click)="activeModal.dismiss()" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
</h4>
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span>
</button>

View File

@@ -2,6 +2,7 @@ import { Component, OnInit, Input } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { IBlock } from '../../blockchain/interfaces';
import { MemPoolService } from '../../services/mem-pool.service';
import { environment } from '../../../environments/environment';
@Component({
selector: 'app-block-modal',
@@ -11,7 +12,7 @@ import { MemPoolService } from '../../services/mem-pool.service';
export class BlockModalComponent implements OnInit {
@Input() block: IBlock;
blockSubsidy = 50;
isEsploraEnabled = !!environment.esplora;
conversions: any;
constructor(

View File

@@ -1 +1,77 @@
<p>block works!</p>
<div class="container">
<h1>Block <ng-template [ngIf]="!isLoadingBlock"><a [routerLink]="['/explorer/block/', block.id]">#{{ block.height }}</a></ng-template></h1>
<ng-template [ngIf]="!isLoadingBlock" [ngIfElse]="loadingBlock">
<br>
<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' }} ({{ block.timestamp | timeSince }} ago)</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>{{ latestBlockHeight - block.height + 1 }} confirmations</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Hash</td>
<td><a [routerLink]="['/explorer/block/', block.id]">{{ block.id | shortenString : 36 }}</a></td>
</tr>
<tr>
<td>Previous Block</td>
<td><a [routerLink]="['/explorer/block/', block.previousblockhash]">{{ block.previousblockhash | shortenString : 36 }}</a></td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h2>{{ transactions?.length || '?' }} of {{ 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 text-light"></div>
<br><br>
</ng-template>
<button *ngIf="transactions?.length" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
</div>
</ng-template>
<ng-template #loadingBlock>
<div class="text-center">
<div class="spinner-border text-light"></div>
<br><br>
</div>
</ng-template>
</div>
<br>

View File

@@ -1,4 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ApiService } from 'src/app/services/api.service';
import { MemPoolService } from 'src/app/services/mem-pool.service';
import { switchMap } from 'rxjs/operators';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-block',
@@ -6,10 +11,55 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./block.component.scss']
})
export class BlockComponent implements OnInit {
block: any;
isLoadingBlock = true;
latestBlockHeight: number;
transactions: any[];
isLoadingTransactions = true;
constructor() { }
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private memPoolService: MemPoolService,
private ref: ChangeDetectorRef,
) { }
ngOnInit() {
this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.isLoadingBlock = true;
const blockHash: string = params.get('id') || '';
this.getBlockTransactions(blockHash);
return this.apiService.getBlock$(blockHash);
})
)
.subscribe((block) => {
this.block = block;
this.isLoadingBlock = false;
this.ref.markForCheck();
});
this.memPoolService.blocks$
.subscribe((block) => {
this.latestBlockHeight = block.height;
});
}
getBlockTransactions(hash: string) {
this.apiService.getBlockTransactions$(hash)
.subscribe((transactions: any) => {
this.transactions = transactions;
this.isLoadingTransactions = false;
});
}
loadMore() {
this.isLoadingTransactions = true;
this.apiService.getBlockTransactions$(this.block.id, this.transactions.length)
.subscribe((transactions) => {
this.transactions = this.transactions.concat(transactions);
this.isLoadingTransactions = false;
});
}
}

View File

@@ -6,6 +6,7 @@ import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { BlockComponent } from './block/block.component';
import { AddressComponent } from './address/address.component';
import { TransactionsListComponent } from './transactions-list/transactions-list.component';
const routes: Routes = [
{
@@ -27,7 +28,7 @@ const routes: Routes = [
];
@NgModule({
declarations: [ExplorerComponent, TransactionComponent, BlockComponent, AddressComponent],
declarations: [ExplorerComponent, TransactionComponent, BlockComponent, AddressComponent, TransactionsListComponent],
imports: [
SharedModule,
CommonModule,

View File

@@ -7,8 +7,8 @@
<th>Timestamp</th>
<th>Mined</th>
<th>Transactions</th>
<th>Size (kB)</th>
<th>Weight (kWU)</th>
<th>Size</th>
<th>Weight</th>
</thead>
<tbody>
<tr *ngFor="let block of blocks; let i= index;">
@@ -17,7 +17,7 @@
<td>{{ block.timestamp | timeSince }} ago </td>
<td>{{ block.tx_count }}</td>
<td>{{ block.size | bytes: 2 }}</td>
<td>{{ block.weight | bytes: 2 }}</td>
<td>{{ block.weight | wuBytes: 2 }}</td>
</tr>
</tbody>
</table>

View File

@@ -1,96 +1,12 @@
<div class="container">
<h1>Transaction</h1>
<br>
<ng-template [ngIf]="!isLoadingTx" [ngIfElse]="loadingTx">
<table class="table table-borderless">
<thead>
<tr class="header-bg">
<th>
{{ tx.txid }}
</th>
</tr>
</thead>
</table>
<app-transactions-list [transactions]="[tx]" [showConfirmations]="true"></app-transactions-list>
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text">
<tbody>
<tr>
<td>
<div *ngFor="let vin of tx.vin">
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
Coinbase
</ng-template>
<ng-template #regularVin>
<a [routerLink]="['/explorer/address/', vin.prevout.scriptpubkey_address]">{{ vin.prevout.scriptpubkey_address }}</a>
(<a [routerLink]="['/explorer/tx/', vin.txid]">tx</a>)
</ng-template>
</div>
</td>
<td class="text-right">
<div *ngFor="let vin of tx.vin">
<ng-template [ngIf]="vin.prevout">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVin">
<span class="green-color">{{ conversions.USD * (vin.prevout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatVin>
{{ vin.prevout.value / 100000000 }} BTC
</ng-template>
</ng-template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless smaller-text">
<tbody>
<tr>
<td>
<div *ngFor="let vout of tx.vout">
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/explorer/address/', vout.scriptpubkey_address]">{{ vout.scriptpubkey_address }}</a>
<ng-template #scriptpubkey_type>
{{ vout.scriptpubkey_type | uppercase }}
</ng-template>
</div>
</td>
<td class="text-right">
<div *ngFor="let vout of tx.vout">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVout">
<span class="green-color">{{ conversions.USD * (vout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatVout>
{{ vout.value / 100000000 }} BTC
</ng-template>
</div>
</td>
</tr>
<tr>
<td class="text-right" colspan="4">
<button *ngIf="tx.status.confirmed" type="button" class="btn btn-success">{{ latestBlockHeight - tx.status.block_height + 1 }} confirmations</button>
<ng-template #unconfirmedButton>
<button type="button" class="btn btn-danger">Unconfirmed</button>
</ng-template>
&nbsp;
<button type="button" class="btn btn-primary" (click)="viewFiat = !viewFiat">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatButton">
<span *ngIf="conversions">{{ conversions.USD * (totalOutput / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatButton>
{{ totalOutput / 100000000 }} BTC
</ng-template>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h2>Details</h2>
@@ -103,11 +19,11 @@
<tbody>
<tr>
<td>Size</td>
<td>{{ tx.size | bytes }}</td>
<td>{{ tx.size | bytes: 2 }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ tx.weight }} WU</td>
<td>{{ tx.weight | wuBytes: 2 }}</td>
</tr>
<tr *ngIf="tx.status.confirmed">
<td>Included in block</td>
@@ -117,7 +33,7 @@
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped" *ngIf="tx.fee">
<table class="table table-borderless table-striped" *ngIf="tx.fee">
<tbody>
<tr>
<td>Fees</td>

View File

@@ -13,10 +13,6 @@ export class TransactionComponent implements OnInit {
tx: any;
isLoadingTx = true;
conversions: any;
totalOutput: number;
viewFiat = false;
latestBlockHeight: number;
constructor(
private route: ActivatedRoute,
@@ -33,7 +29,6 @@ export class TransactionComponent implements OnInit {
)
.subscribe((tx) => {
this.tx = tx;
this.totalOutput = this.tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
this.isLoadingTx = false;
});
@@ -41,10 +36,5 @@ export class TransactionComponent implements OnInit {
.subscribe((conversions) => {
this.conversions = conversions;
});
this.memPoolService.blocks$
.subscribe((block) => {
this.latestBlockHeight = block.height;
});
}
}

View File

@@ -0,0 +1,94 @@
<ng-template ngFor let-tx [ngForOf]="transactions">
<table class="table table-borderless">
<thead>
<tr class="header-bg">
<th>
<a [routerLink]="['/explorer/tx/', tx.txid]">{{ tx.txid }}</a>
</th>
</tr>
</thead>
</table>
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text">
<tbody>
<tr>
<td>
<div *ngFor="let vin of tx.vin">
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
Coinbase
</ng-template>
<ng-template #regularVin>
<a [routerLink]="['/explorer/address/', vin.prevout.scriptpubkey_address]">{{ vin.prevout.scriptpubkey_address }}</a>
(<a [routerLink]="['/explorer/tx/', vin.txid]">tx</a>)
</ng-template>
</div>
</td>
<td class="text-right">
<div *ngFor="let vin of tx.vin">
<ng-template [ngIf]="vin.prevout">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVin">
<span class="green-color">{{ conversions.USD * (vin.prevout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatVin>
{{ vin.prevout.value / 100000000 }} BTC
</ng-template>
</ng-template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless smaller-text">
<tbody>
<tr>
<td>
<div *ngFor="let vout of tx.vout">
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/explorer/address/', vout.scriptpubkey_address]">{{ vout.scriptpubkey_address }}</a>
<ng-template #scriptpubkey_type>
{{ vout.scriptpubkey_type | uppercase }}
</ng-template>
</div>
</td>
<td class="text-right">
<div *ngFor="let vout of tx.vout">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVout">
<span class="green-color">{{ conversions.USD * (vout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatVout>
{{ vout.value / 100000000 }} BTC
</ng-template>
</div>
</td>
</tr>
<tr>
<td class="text-right" colspan="4">
<ng-template [ngIf]="showConfirmations">
<button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-success">{{ latestBlockHeight - tx.status.block_height + 1 }} confirmations</button>
<ng-template #unconfirmedButton>
<button type="button" class="btn btn-danger">Unconfirmed</button>
</ng-template>
&nbsp;
</ng-template>
<button type="button" class="btn btn-primary" (click)="viewFiat = !viewFiat">
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatButton">
<span *ngIf="conversions">{{ conversions.USD * (getTotalTxOutput(tx) / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-template>
<ng-template #viewFiatButton>
{{ getTotalTxOutput(tx) / 100000000 }} BTC
</ng-template>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,3 @@
.header-bg {
background-color: #181b2d !important;
}

View File

@@ -0,0 +1,36 @@
import { Component, OnInit, Input } from '@angular/core';
import { MemPoolService } from 'src/app/services/mem-pool.service';
@Component({
selector: 'app-transactions-list',
templateUrl: './transactions-list.component.html',
styleUrls: ['./transactions-list.component.scss']
})
export class TransactionsListComponent implements OnInit {
@Input() transactions: any[];
@Input() showConfirmations = false;
latestBlockHeight: number;
viewFiat = false;
conversions: any;
constructor(
private memPoolService: MemPoolService,
) { }
ngOnInit() {
this.memPoolService.conversions$
.subscribe((conversions) => {
this.conversions = conversions;
});
this.memPoolService.blocks$
.subscribe((block) => {
this.latestBlockHeight = block.height;
});
}
getTotalTxOutput(tx: any) {
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
}
}

View File

@@ -162,11 +162,19 @@ export class ApiService {
return this.httpClient.get<IMempoolStats[]>(API_BASE_URL + '/statistics/6m');
}
listBlocks$(height?: number): Observable<IBlockTransaction[]> {
return this.httpClient.get<IBlockTransaction[]>(API_BASE_URL + '/explorer/blocks/' + (height || ''));
listBlocks$(height?: number): Observable<any[]> {
return this.httpClient.get<any[]>(API_BASE_URL + '/explorer/blocks/' + (height || ''));
}
getTransaction$(txId: string): Observable<IBlockTransaction[]> {
return this.httpClient.get<IBlockTransaction[]>(API_BASE_URL + '/explorer/tx/' + txId);
getTransaction$(txId: string): Observable<any[]> {
return this.httpClient.get<any[]>(API_BASE_URL + '/explorer/tx/' + txId);
}
getBlock$(hash: string): Observable<any[]> {
return this.httpClient.get<any[]>(API_BASE_URL + '/explorer/block/' + hash);
}
getBlockTransactions$(hash: string, index?: number): Observable<any[]> {
return this.httpClient.get<any[]>(API_BASE_URL + '/explorer/block/' + hash + '/tx/' + (index || ''));
}
}

View File

@@ -0,0 +1,63 @@
/* tslint:disable */
import { Pipe, PipeTransform } from '@angular/core';
import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils';
export type ByteUnit = 'WU' | 'kWU' | 'MWU' | 'GWU' | 'TWU';
@Pipe({
name: 'wuBytes'
})
export class WuBytesPipe implements PipeTransform {
static formats: { [key: string]: { max: number, prev?: ByteUnit } } = {
'WU': {max: 1000},
'kWU': {max: Math.pow(1000, 2), prev: 'WU'},
'MWU': {max: Math.pow(1000, 3), prev: 'kWU'},
'GWU': {max: Math.pow(1000, 4), prev: 'MWU'},
'TWU': {max: Number.MAX_SAFE_INTEGER, prev: 'GWU'}
};
transform(input: any, decimal: number = 0, from: ByteUnit = 'WU', to?: ByteUnit): any {
if (!(isNumberFinite(input) &&
isNumberFinite(decimal) &&
isInteger(decimal) &&
isPositive(decimal))) {
return input;
}
let bytes = input;
let unit = from;
while (unit !== 'WU') {
bytes *= 1024;
unit = WuBytesPipe.formats[unit].prev!;
}
if (to) {
const format = WuBytesPipe.formats[to];
const result = toDecimal(WuBytesPipe.calculateResult(format, bytes), decimal);
return WuBytesPipe.formatResult(result, to);
}
for (const key in WuBytesPipe.formats) {
const format = WuBytesPipe.formats[key];
if (bytes < format.max) {
const result = toDecimal(WuBytesPipe.calculateResult(format, bytes), decimal);
return WuBytesPipe.formatResult(result, key);
}
}
}
static formatResult(result: number, unit: string): string {
return `${result} ${unit}`;
}
static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) {
const prev = format.prev ? WuBytesPipe.formats[format.prev] : undefined;
return prev ? bytes / prev.max : bytes;
}
}

View File

@@ -0,0 +1,9 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'shortenString' })
export class ShortenStringPipe implements PipeTransform {
transform(str: string, length: number = 12) {
const half = length / 2;
return str.substring(0, half) + '...' + str.substring(str.length - half);
}
}

View File

@@ -8,6 +8,8 @@ import { RoundPipe } from './pipes/math-round-pipe/math-round.pipe';
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
import { ChartistComponent } from '../statistics/chartist.component';
import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
@NgModule({
imports: [
@@ -21,6 +23,8 @@ import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
CeilPipe,
BytesPipe,
VbytesPipe,
WuBytesPipe,
ShortenStringPipe,
TimeSincePipe,
],
exports: [
@@ -28,7 +32,9 @@ import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
CeilPipe,
BytesPipe,
VbytesPipe,
WuBytesPipe,
TimeSincePipe,
ShortenStringPipe,
NgbButtonsModule,
NgbModalModule,
ChartistComponent,
@@ -36,6 +42,8 @@ import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
providers: [
BytesPipe,
VbytesPipe,
WuBytesPipe,
ShortenStringPipe,
]
})
export class SharedModule { }

View File

@@ -4,8 +4,8 @@
<tr>
<td class="text-left"><b>Transaction hash</b></td>
<td class="text-right">
<a *ngIf="!isEsploraEnabled" href="https://www.blockstream.info/tx/{{ tx?.txid }}" target="_blank">{{ txIdShort }}</a>
<a *ngIf="isEsploraEnabled" [routerLink]="['/explorer/tx/', tx?.txid]">{{ txIdShort }}</a>
<a *ngIf="!isEsploraEnabled" href="https://www.blockstream.info/tx/{{ tx?.txid }}" target="_blank">{{ tx?.txid | shortenString }}</a>
<a *ngIf="isEsploraEnabled" [routerLink]="['/explorer/tx/', tx?.txid]">{{ tx?.txid | shortenString }}</a>
</td>
</tr>
<tr>

View File

@@ -61,9 +61,6 @@ export class TxBubbleComponent implements OnInit, OnDestroy {
if (this.txShowTxNotFound) {
setTimeout(() => { this.txShowTxNotFound = false; }, 2000);
}
if (this.tx) {
this.txIdShort = this.tx.txid.substring(0, 6) + '...' + this.tx.txid.substring(this.tx.txid.length - 6);
}
if (this.latestBlockHeight) {
this.confirmations = (this.latestBlockHeight - this.txTrackingBlockHeight) + 1;
}