Explorer page with latest blocks. WIP

This commit is contained in:
Simon Lindh 2019-11-06 15:35:02 +08:00
parent 7344c518d3
commit 02d67e8406
22 changed files with 225 additions and 20 deletions

View File

@ -7,4 +7,7 @@ export interface AbstractBitcoinApi {
getBlockCount(): Promise<number>;
getBlock(hash: string): Promise<IBlock>;
getBlockHash(height: number): Promise<string>;
getBlocks(): Promise<string>;
getBlocksFromHeight(height: number): Promise<string>;
}

View File

@ -80,6 +80,14 @@ class BitcoindApi implements AbstractBitcoinApi {
});
});
}
getBlocks(): Promise<string> {
throw new Error('Method not implemented.');
}
getBlocksFromHeight(height: number): Promise<string> {
throw new Error('Method not implemented.');
}
}
export default BitcoindApi;

View File

@ -94,6 +94,28 @@ class EsploraApi implements AbstractBitcoinApi {
}
});
}
getBlocks(): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/blocks');
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
getBlocksFromHeight(height: number): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/blocks/' + height);
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
}
export default EsploraApi;

View File

@ -263,7 +263,15 @@ class MempoolSpace {
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
;
if (config.BACKEND_API === 'esplora') {
this.app
.get(config.API_ENDPOINT + 'explorer/blocks', routes.getBlocks)
.get(config.API_ENDPOINT + 'explorer/blocks/:height', routes.getBlocks)
;
}
}
}
}
const mempoolSpace = new MempoolSpace();

View File

@ -1,6 +1,7 @@
import statistics from './api/statistics';
import feeApi from './api/fee-api';
import projectedBlocks from './api/projected-blocks';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
class Routes {
private cache = {};
@ -75,6 +76,20 @@ class Routes {
res.status(500).send(e.message);
}
}
public async getBlocks(req, res) {
try {
let result: string;
if (req.params.height) {
result = await bitcoinApi.getBlocksFromHeight(req.params.height);
} else {
result = await bitcoinApi.getBlocks();
}
res.send(result);
} catch (e) {
res.status(500).send(e.message);
}
}
}
export default new Routes();

View File

@ -34,6 +34,10 @@ const routes: Routes = [
path: 'graphs',
component: StatisticsComponent,
},
{
path: 'explorer',
loadChildren: './explorer/explorer.module#ExplorerModule',
},
],
},
{

View File

@ -13,7 +13,7 @@
<div class="block-size">{{ block.size | bytes: 2 }}</div>
<div class="transaction-count">{{ block.nTx }} transactions</div>
<br /><br />
<div class="time-difference">{{ getTimeSinceMined(block) }} ago</div>
<div class="time-difference">{{ block.time | timeSince }} ago</div>
</div>
</div>
</div>

View File

@ -34,23 +34,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
this.blocksSubscription.unsubscribe();
}
getTimeSinceMined(block: IBlock): string {
const minutes = ((new Date().getTime()) - (new Date(block.time * 1000).getTime())) / 1000 / 60;
if (minutes >= 120) {
return Math.floor(minutes / 60) + ' hours';
}
if (minutes >= 60) {
return Math.floor(minutes / 60) + ' hour';
}
if (minutes <= 1) {
return '< 1 minute';
}
if (minutes === 1) {
return '1 minute';
}
return Math.round(minutes) + ' minutes';
}
trackByBlocksFn(index: number, item: IBlock) {
return item.height;
}

View File

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

View File

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

View File

@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ExplorerComponent } from './explorer/explorer.component';
import { TransactionComponent } from './transaction/transaction.component';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { BlockComponent } from './block/block.component';
const routes: Routes = [
{
path: '',
component: ExplorerComponent,
},
{
path: 'block/:id',
component: BlockComponent,
},
{
path: 'tx/:id',
component: TransactionComponent,
},
];
@NgModule({
declarations: [ExplorerComponent, TransactionComponent, BlockComponent],
imports: [
SharedModule,
CommonModule,
RouterModule.forChild(routes),
]
})
export class ExplorerModule { }

View File

@ -0,0 +1,34 @@
<div class="container">
<h1>Latest blocks</h1>
<table class="table table-borderless">
<thead>
<th>Height</th>
<th>Timestamp</th>
<th>Mined</th>
<th>Transactions</th>
<th>Size (kB)</th>
<th>Weight (kWU)</th>
</thead>
<tbody>
<tr *ngFor="let block of blocks; let i= index;">
<td><a [routerLink]="['./block', block.id]">#{{ block.height }}</a></td>
<td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td>{{ block.timestamp | timeSince }} ago </td>
<td>{{ block.tx_count }}</td>
<td>{{ block.size | bytes: 2 }}</td>
<td>{{ block.weight | bytes: 2 }}</td>
</tr>
</tbody>
</table>
<div class="text-center">
<ng-template [ngIf]="isLoading">
<div class="spinner-border text-light"></div>
<br><br>
</ng-template>
<button *ngIf="blocks.length" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
</div>
<br>
</div>

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-explorer',
templateUrl: './explorer.component.html',
styleUrls: ['./explorer.component.scss']
})
export class ExplorerComponent implements OnInit {
blocks: any[] = [];
isLoading = true;
constructor(
private apiService: ApiService,
) { }
ngOnInit() {
this.apiService.listBlocks$()
.subscribe((blocks) => {
this.blocks = blocks;
this.isLoading = false;
});
}
loadMore() {
this.isLoading = true;
this.apiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1)
.subscribe((blocks) => {
this.blocks = this.blocks.concat(blocks);
this.isLoading = false;
});
}
}

View File

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

View File

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

View File

@ -18,12 +18,15 @@
<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">
<a class="nav-link" routerLink="/explorer" (click)="collapse()">Explorer</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
</li>
</ul>
<form [formGroup]="searchForm" class="form-inline mt-2 mt-md-0 mr-4" (submit)="searchForm.valid && search()" novalidate>
<input formControlName="txId" required style="width: 300px;" class="form-control mr-sm-2" type="text" placeholder="Track transaction (TXID)" aria-label="Search">
<input formControlName="txId" required style="width: 300px;" class="form-control mr-sm-2" type="text" placeholder="Search transaction ID" aria-label="Search">
<button class="btn btn-primary my-2 my-sm-0" type="submit">Track</button>
</form>
</div>

View File

@ -162,4 +162,8 @@ 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 || ''));
}
}

View File

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'timeSince' })
export class TimeSincePipe implements PipeTransform {
transform(timestamp: number) {
const minutes = ((new Date().getTime()) - (new Date(timestamp * 1000).getTime())) / 1000 / 60;
if (minutes >= 120) {
return Math.floor(minutes / 60) + ' hours';
}
if (minutes >= 60) {
return Math.floor(minutes / 60) + ' hour';
}
if (minutes <= 1) {
return '< 1 minute';
}
if (minutes === 1) {
return '1 minute';
}
return Math.round(minutes) + ' minutes';
}
}

View File

@ -7,6 +7,7 @@ import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
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';
@NgModule({
imports: [
@ -20,12 +21,14 @@ import { ChartistComponent } from '../statistics/chartist.component';
CeilPipe,
BytesPipe,
VbytesPipe,
TimeSincePipe,
],
exports: [
RoundPipe,
CeilPipe,
BytesPipe,
VbytesPipe,
TimeSincePipe,
NgbButtonsModule,
NgbModalModule,
ChartistComponent,