mirror of
https://github.com/mempool/mempool.git
synced 2025-01-18 21:32:55 +01:00
Explorer page with latest blocks. WIP
This commit is contained in:
parent
7344c518d3
commit
02d67e8406
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -34,6 +34,10 @@ const routes: Routes = [
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'explorer',
|
||||
loadChildren: './explorer/explorer.module#ExplorerModule',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
1
frontend/src/app/explorer/block/block.component.html
Normal file
1
frontend/src/app/explorer/block/block.component.html
Normal file
@ -0,0 +1 @@
|
||||
<p>block works!</p>
|
15
frontend/src/app/explorer/block/block.component.ts
Normal file
15
frontend/src/app/explorer/block/block.component.ts
Normal 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() {
|
||||
}
|
||||
|
||||
}
|
32
frontend/src/app/explorer/explorer.module.ts
Normal file
32
frontend/src/app/explorer/explorer.module.ts
Normal 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 { }
|
34
frontend/src/app/explorer/explorer/explorer.component.html
Normal file
34
frontend/src/app/explorer/explorer/explorer.component.html
Normal 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>
|
33
frontend/src/app/explorer/explorer/explorer.component.ts
Normal file
33
frontend/src/app/explorer/explorer/explorer.component.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<p>transaction works!</p>
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
@ -18,12 +18,15 @@
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/tv" (click)="collapse()">TV view <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>
|
||||
|
@ -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 || ''));
|
||||
}
|
||||
|
||||
}
|
||||
|
21
frontend/src/app/shared/pipes/time-since/time-since.pipe.ts
Normal file
21
frontend/src/app/shared/pipes/time-since/time-since.pipe.ts
Normal 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';
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user