Transaction view.

This commit is contained in:
Simon Lindh 2019-11-10 16:44:00 +08:00
parent 02d67e8406
commit bd2bd478ef
26 changed files with 287 additions and 37 deletions

View File

@ -268,6 +268,7 @@ class MempoolSpace {
this.app
.get(config.API_ENDPOINT + 'explorer/blocks', routes.getBlocks)
.get(config.API_ENDPOINT + 'explorer/blocks/:height', routes.getBlocks)
.get(config.API_ENDPOINT + 'explorer/tx/:id', routes.getRawTransaction)
;
}

View File

@ -90,6 +90,15 @@ class Routes {
res.status(500).send(e.message);
}
}
public async getRawTransaction(req, res) {
try {
const result = await bitcoinApi.getRawTransaction(req.params.id);
res.send(result);
} catch (e) {
res.status(500).send(e.message);
}
}
}
export default new Routes();

View File

@ -48,6 +48,23 @@
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
},
"esplora": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment-esplora.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
},

View File

@ -5,7 +5,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve --aot --proxy-config proxy.conf.json",
"build": "ng build --prod --vendorChunk=false --build-optimizer=true",
"build": "ng build --prod",
"build-esplora": "ng build --prod --configuration=esplora",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"

View File

@ -1,7 +1 @@
.yellow-color {
color: #ffd800;
}
.green-color {
color: #3bcc49;
}

View File

@ -2,7 +2,8 @@
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
<div (click)="openBlockModal(block);" class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
<div class="block-height">
<a href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
<a *ngIf="!isEsploraEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
<a *ngIf="isEsploraEnabled" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
</div>
<div class="block-body">
<div class="fees">
@ -13,7 +14,7 @@
<div class="block-size">{{ block.size | bytes: 2 }}</div>
<div class="transaction-count">{{ block.nTx }} transactions</div>
<br /><br />
<div class="time-difference">{{ block.time | timeSince }} ago</div>
<div class="time-difference">{{ block.time | timeSince : trigger }} ago</div>
</div>
</div>
</div>

View File

@ -39,10 +39,6 @@
margin-bottom: 2px;
}
.yellow-color {
color: #ffd800;
}
.transaction-count {
font-size: 12px;
}

View File

@ -4,6 +4,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { BlockModalComponent } from './block-modal/block-modal.component';
import { MemPoolService } from '../services/mem-pool.service';
import { Subscription } from 'rxjs';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-blockchain-blocks',
@ -13,6 +14,9 @@ import { Subscription } from 'rxjs';
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
blocks: IBlock[] = [];
blocksSubscription: Subscription;
interval: any;
trigger = 0;
isEsploraEnabled = !!environment.esplora;
constructor(
private modalService: NgbModal,
@ -28,10 +32,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 8);
});
this.interval = setInterval(() => this.trigger++, 10 * 1000);
}
ngOnDestroy() {
this.blocksSubscription.unsubscribe();
clearInterval(this.interval);
}
trackByBlocksFn(index: number, item: IBlock) {

View File

@ -49,10 +49,6 @@
margin-bottom: 2px;
}
.yellow-color {
color: #ffd800;
}
.transaction-count {
font-size: 12px;
}

View File

@ -1,7 +0,0 @@
.yellow-color {
color: #ffd800;
}
.green-color {
color: #3bcc49;
}

View File

@ -0,0 +1 @@
<p>address works!</p>

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-address',
templateUrl: './address.component.html',
styleUrls: ['./address.component.scss']
})
export class AddressComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -5,6 +5,7 @@ import { TransactionComponent } from './transaction/transaction.component';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { BlockComponent } from './block/block.component';
import { AddressComponent } from './address/address.component';
const routes: Routes = [
{
@ -19,10 +20,14 @@ const routes: Routes = [
path: 'tx/:id',
component: TransactionComponent,
},
{
path: 'address/:id',
component: AddressComponent,
},
];
@NgModule({
declarations: [ExplorerComponent, TransactionComponent, BlockComponent],
declarations: [ExplorerComponent, TransactionComponent, BlockComponent, AddressComponent],
imports: [
SharedModule,
CommonModule,

View File

@ -1 +1,143 @@
<p>transaction works!</p>
<div class="container">
<h1>Transaction</h1>
<ng-template [ngIf]="!isLoadingTx" [ngIfElse]="loadingTx">
<table class="table table-borderless">
<thead>
<tr class="header-bg">
<th>
{{ tx.txid }}
</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">
<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>
<br>
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Size</td>
<td>{{ tx.size | bytes }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ tx.weight }} WU</td>
</tr>
<tr *ngIf="tx.status.confirmed">
<td>Included in block</td>
<td><a [routerLink]="['/explorer/block/', tx.status.block_hash]">#{{ tx.status.block_height }}</a></td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped" *ngIf="tx.fee">
<tbody>
<tr>
<td>Fees</td>
<td>{{ tx.fee }} <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee | currency:'USD':'symbol':'1.2-2' }}</span>)</span></td>
</tr>
<tr>
<td>Fees per vByte</td>
<td>{{ (tx.fee * 100000000) / tx.vsize | number : '1.2-2' }} sat/vB</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
<ng-template #loadingTx>
<div class="text-center">
<div class="spinner-border text-light"></div>
<br><br>
</div>
</ng-template>
</div>

View File

@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { ApiService } from 'src/app/services/api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { MemPoolService } from 'src/app/services/mem-pool.service';
@Component({
selector: 'app-transaction',
@ -6,10 +10,41 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./transaction.component.scss']
})
export class TransactionComponent implements OnInit {
tx: any;
isLoadingTx = true;
conversions: any;
totalOutput: number;
constructor() { }
viewFiat = false;
latestBlockHeight: number;
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private memPoolService: MemPoolService,
) { }
ngOnInit() {
}
this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const txId: string = params.get('id') || '';
return this.apiService.getTransaction$(txId);
})
)
.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;
});
this.memPoolService.conversions$
.subscribe((conversions) => {
this.conversions = conversions;
});
this.memPoolService.blocks$
.subscribe((block) => {
this.latestBlockHeight = block.height;
});
}
}

View File

@ -18,7 +18,7 @@
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/tv" (click)="collapse()">TV view &nbsp;<img src="./assets/expand.png" width="15"/></a>
</li>
<li class="nav-item" routerLinkActive="active">
<li class="nav-item" routerLinkActive="active" *ngIf="isEsploraEnabled">
<a class="nav-link" routerLink="/explorer" (click)="collapse()">Explorer</a>
</li>
<li class="nav-item" routerLinkActive="active">

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { MemPoolService } from '../services/mem-pool.service';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-master-page',
@ -13,6 +14,7 @@ export class MasterPageComponent implements OnInit {
navCollapsed = false;
isOffline = false;
searchForm: FormGroup;
isEsploraEnabled = !!environment.esplora;
constructor(
private memPoolService: MemPoolService,

View File

@ -166,4 +166,7 @@ export class ApiService {
return this.httpClient.get<IBlockTransaction[]>(API_BASE_URL + '/explorer/blocks/' + (height || ''));
}
getTransaction$(txId: string): Observable<IBlockTransaction[]> {
return this.httpClient.get<IBlockTransaction[]>(API_BASE_URL + '/explorer/tx/' + txId);
}
}

View File

@ -3,7 +3,10 @@
<table style="width: 100%;">
<tr>
<td class="text-left"><b>Transaction hash</b></td>
<td class="text-right"><a href="https://www.blockstream.info/tx/{{ tx?.txid }}" target="_blank">{{ txIdShort }}</a></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>
</td>
</tr>
<tr>
<td class="text-left"><b>Fee:</b></td>

View File

@ -63,7 +63,3 @@
.txBubble .arrow-top-left.txBubbleText::after {
left: 20%;
}
.green-color {
color: #3bcc49;
}

View File

@ -2,6 +2,7 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ITransaction, IProjectedBlock } from '../blockchain/interfaces';
import { Subscription } from 'rxjs';
import { ITxTracking, MemPoolService } from '../services/mem-pool.service';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-tx-bubble',
@ -35,6 +36,8 @@ export class TxBubbleComponent implements OnInit, OnDestroy {
txTrackingTx: ITransaction | null = null;
txShowTxNotFound = false;
isEsploraEnabled = !!environment.esplora;
@HostListener('window:resize', ['$event'])
onResize(event: Event) {
this.moveTxBubbleToPosition();

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
esplora: true,
};

View File

@ -1,3 +1,4 @@
export const environment = {
production: true
production: true,
esplora: false,
};

View File

@ -3,7 +3,8 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
esplora: true,
};
/*

View File

@ -132,4 +132,28 @@ html, body {
hr {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
}
.green-color {
color: #3bcc49;
}
.yellow-color {
color: #ffd800;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #181b2d !important;
}
.header-bg {
background-color: #653b9c;
}
.bordertop {
border-top: 1px solid #4c4c4c;
}
.smaller-text {
font-size: 14px;
}