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>;
|
getBlockCount(): Promise<number>;
|
||||||
getBlock(hash: string): Promise<IBlock>;
|
getBlock(hash: string): Promise<IBlock>;
|
||||||
getBlockHash(height: number): Promise<string>;
|
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;
|
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;
|
export default EsploraApi;
|
||||||
|
@ -263,7 +263,15 @@ class MempoolSpace {
|
|||||||
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
|
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
|
||||||
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.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();
|
const mempoolSpace = new MempoolSpace();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import statistics from './api/statistics';
|
import statistics from './api/statistics';
|
||||||
import feeApi from './api/fee-api';
|
import feeApi from './api/fee-api';
|
||||||
import projectedBlocks from './api/projected-blocks';
|
import projectedBlocks from './api/projected-blocks';
|
||||||
|
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
private cache = {};
|
private cache = {};
|
||||||
@ -75,6 +76,20 @@ class Routes {
|
|||||||
res.status(500).send(e.message);
|
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();
|
export default new Routes();
|
||||||
|
@ -34,6 +34,10 @@ const routes: Routes = [
|
|||||||
path: 'graphs',
|
path: 'graphs',
|
||||||
component: StatisticsComponent,
|
component: StatisticsComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'explorer',
|
||||||
|
loadChildren: './explorer/explorer.module#ExplorerModule',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
||||||
<div class="transaction-count">{{ block.nTx }} transactions</div>
|
<div class="transaction-count">{{ block.nTx }} transactions</div>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<div class="time-difference">{{ getTimeSinceMined(block) }} ago</div>
|
<div class="time-difference">{{ block.time | timeSince }} ago</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,23 +34,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.blocksSubscription.unsubscribe();
|
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) {
|
trackByBlocksFn(index: number, item: IBlock) {
|
||||||
return item.height;
|
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">
|
<li class="nav-item" routerLinkActive="active">
|
||||||
<a class="nav-link" routerLink="/tv" (click)="collapse()">TV view <img src="./assets/expand.png" width="15"/></a>
|
<a class="nav-link" routerLink="/tv" (click)="collapse()">TV view <img src="./assets/expand.png" width="15"/></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" routerLinkActive="active">
|
||||||
|
<a class="nav-link" routerLink="/explorer" (click)="collapse()">Explorer</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" routerLinkActive="active">
|
<li class="nav-item" routerLinkActive="active">
|
||||||
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
|
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form [formGroup]="searchForm" class="form-inline mt-2 mt-md-0 mr-4" (submit)="searchForm.valid && search()" novalidate>
|
<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>
|
<button class="btn btn-primary my-2 my-sm-0" type="submit">Track</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -162,4 +162,8 @@ export class ApiService {
|
|||||||
return this.httpClient.get<IMempoolStats[]>(API_BASE_URL + '/statistics/6m');
|
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 { RoundPipe } from './pipes/math-round-pipe/math-round.pipe';
|
||||||
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
|
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
|
||||||
import { ChartistComponent } from '../statistics/chartist.component';
|
import { ChartistComponent } from '../statistics/chartist.component';
|
||||||
|
import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -20,12 +21,14 @@ import { ChartistComponent } from '../statistics/chartist.component';
|
|||||||
CeilPipe,
|
CeilPipe,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
VbytesPipe,
|
VbytesPipe,
|
||||||
|
TimeSincePipe,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RoundPipe,
|
RoundPipe,
|
||||||
CeilPipe,
|
CeilPipe,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
VbytesPipe,
|
VbytesPipe,
|
||||||
|
TimeSincePipe,
|
||||||
NgbButtonsModule,
|
NgbButtonsModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
ChartistComponent,
|
ChartistComponent,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user