mirror of
https://github.com/mempool/mempool.git
synced 2025-01-18 05:12:35 +01:00
Refactor. API explanations. UX revamp.
This commit is contained in:
parent
acd658a0e7
commit
34645908e9
@ -1,5 +1,5 @@
|
||||
const config = require('../../../mempool-config.json');
|
||||
import { Transaction, Block } from '../../interfaces';
|
||||
import { Transaction, Block, MempoolInfo } from '../../interfaces';
|
||||
import * as request from 'request';
|
||||
|
||||
class ElectrsApi {
|
||||
@ -7,6 +7,27 @@ class ElectrsApi {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
getMempoolInfo(): Promise<MempoolInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/mempool', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
if (!response.count) {
|
||||
reject('Empty data');
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
size: response.count,
|
||||
bytes: response.vsize,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRawMempool(): Promise<Transaction['txid'][]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000, forever: true }, (err, res, response) => {
|
||||
|
@ -32,6 +32,14 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateMemPoolInfo() {
|
||||
try {
|
||||
this.mempoolInfo = await bitcoinApi.getMempoolInfo();
|
||||
} catch (err) {
|
||||
console.log('Error getMempoolInfo', err);
|
||||
}
|
||||
}
|
||||
|
||||
public getMempoolInfo(): MempoolInfo | undefined {
|
||||
return this.mempoolInfo;
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ class Server {
|
||||
}
|
||||
|
||||
private async runMempoolIntervalFunctions() {
|
||||
await memPool.updateMemPoolInfo();
|
||||
await blocks.updateBlocks();
|
||||
await memPool.updateMempool();
|
||||
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.ELECTRS_POLL_RATE_MS);
|
||||
@ -83,28 +84,33 @@ class Server {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
|
||||
if (parsedMessage.action === 'want') {
|
||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
||||
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
||||
client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
|
||||
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
|
||||
client['txId'] = parsedMessage.txId;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
const _blocks = blocks.getBlocks();
|
||||
if (!_blocks) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'blocks': _blocks,
|
||||
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
}));
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
statistics.setNewStatisticsEntryCallback((stats: Statistic) => {
|
||||
@ -113,11 +119,13 @@ class Server {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client['want-live-2h-chart']) {
|
||||
if (!client['want-live-2h-chart']) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -127,6 +135,10 @@ class Server {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client['want-blocks']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client['txId'] && txIds.indexOf(client['txId']) > -1) {
|
||||
client['txId'] = null;
|
||||
client.send(JSON.stringify({
|
||||
@ -143,16 +155,29 @@ class Server {
|
||||
|
||||
memPool.setMempoolChangedCallback((newMempool: { [txid: string]: SimpleTransaction }) => {
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||
const pBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'mempool-blocks': pBlocks
|
||||
}));
|
||||
const response = {};
|
||||
|
||||
if (client['want-stats']) {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
}
|
||||
|
||||
if (client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { MasterPageComponent } from './components/master-page/master-page.compon
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
import { ExplorerComponent } from './components/explorer/explorer.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -18,6 +19,10 @@ const routes: Routes = [
|
||||
path: '',
|
||||
component: StartComponent,
|
||||
},
|
||||
{
|
||||
path: 'explorer',
|
||||
component: ExplorerComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
|
@ -38,6 +38,8 @@ import { StatisticsComponent } from './components/statistics/statistics.componen
|
||||
import { ChartistComponent } from './components/statistics/chartist.component';
|
||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { ExplorerComponent } from './components/explorer/explorer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -68,7 +70,9 @@ import { BlockchainComponent } from './components/blockchain/blockchain.componen
|
||||
LatestTransactionsComponent,
|
||||
QrcodeComponent,
|
||||
ClipboardComponent,
|
||||
ExplorerComponent,
|
||||
ChartistComponent,
|
||||
FooterComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -1,34 +1,54 @@
|
||||
<div class="text-center">
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<img src="./assets/mempool-tube.png" width="63" height="63" />
|
||||
<br /><br />
|
||||
|
||||
<h2>About</h2>
|
||||
<h1>About</h1>
|
||||
|
||||
<p>Mempool.Space is a realtime Bitcoin blockchain explorer and mempool visualizer.</p>
|
||||
<p>Created by <a href="https://twitter.com/softbtc">@softbtc</a>
|
||||
<br />Hosted by <a href="https://twitter.com/wiz">@wiz</a>
|
||||
<br />Designed by <a href="https://instagram.com/markjborg">@markjborg</a>
|
||||
|
||||
|
||||
<h2>Fee API</h2>
|
||||
|
||||
<div class="col-4 mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/recommended" readonly>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<h2>HTTP API</h2>
|
||||
|
||||
<h1>Donate</h1>
|
||||
<img src="./assets/btc-qr-code-segwit.png" width="200" height="200" />
|
||||
<br />
|
||||
bc1qqrmgr60uetlmrpylhtllawyha9z5gw6hwdmk2t
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td style="width: 50%;">Fee API</td>
|
||||
<td>
|
||||
<div class="mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/recommended" readonly>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mempool blocks</td>
|
||||
<td>
|
||||
<div class="mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/mempool-blocks" readonly>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>WebSocket API</h2>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td style="width: 50%;">
|
||||
<span class="text-small">
|
||||
Upon connection, send object <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
|
||||
to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mx-auto">
|
||||
<input class="form-control" type="text" value="wss://mempool.space/ws" readonly>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<h3>PayNym</h3>
|
||||
<img src="./assets/paynym-code.png" width="200" height="200" />
|
||||
<br />
|
||||
<p style="word-wrap: break-word; overflow-wrap: break-word;max-width: 300px; text-align: center; margin: auto;">
|
||||
PM8TJZWDn1XbYmVVMR3RP9Kt1BW69VCSLTC12UB8iWUiKcEBJsxB4UUKBMJxc3LVaxtU5d524sLFrTy9kFuyPQ73QkEagGcMfCE6M38E5C67EF8KAqvS
|
||||
</p>
|
||||
</div>
|
||||
|
@ -0,0 +1,8 @@
|
||||
.text-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code {
|
||||
background-color: #1d1f31;
|
||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
|
||||
}
|
@ -13,7 +13,7 @@ export class AboutComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want([]);
|
||||
this.websocketService.want(['blocks']);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="container">
|
||||
|
||||
<app-blockchain></app-blockchain>
|
||||
<app-blockchain position="top"></app-blockchain>
|
||||
|
||||
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/', blockHash]">#{{ blockHeight }}</a></ng-template></h1>
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { switchMap } from 'rxjs/operators';
|
||||
import { Block, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@ -25,9 +26,12 @@ export class BlockComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash: string = params.get('id') || '';
|
||||
@ -40,6 +44,7 @@ export class BlockComponent implements OnInit {
|
||||
this.blockHash = blockHash;
|
||||
|
||||
if (history.state.data && history.state.data.block) {
|
||||
this.blockHeight = history.state.data.block.height;
|
||||
return of(history.state.data.block);
|
||||
} else {
|
||||
this.isLoadingBlock = true;
|
||||
|
@ -1,6 +1,7 @@
|
||||
<div class="blocks-container" *ngIf="blocks.length">
|
||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
||||
<div [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }" class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
|
||||
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
|
||||
<a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }" class="blockLink"> </a>
|
||||
<div class="block-height">
|
||||
<a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a>
|
||||
</div>
|
||||
|
@ -1,7 +1,14 @@
|
||||
.bitcoin-block {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blockLink {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mined-block {
|
||||
|
@ -1,16 +1,10 @@
|
||||
<div *ngIf="isLoading" class="text-center">
|
||||
<h3>Loading blocks...</h3>
|
||||
<div *ngIf="isLoading" class="loading-block">
|
||||
<h3>Waiting for blocks...</h3>
|
||||
<br>
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
<div *ngIf="!isLoading && txTrackingLoading" class="text-center black-background">
|
||||
<h3>Locating transaction...</h3>
|
||||
</div>
|
||||
<div *ngIf="txShowTxNotFound" class="text-center black-background">
|
||||
<h3>Transaction not found!</h3>
|
||||
</div>
|
||||
<div class="text-center" class="blockchain-wrapper">
|
||||
<div class="position-container">
|
||||
<div class="position-container" [ngStyle]="{'top': position === 'top' ? '100px' : 'calc(50% - 60px)'}">
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
<app-blockchain-blocks></app-blockchain-blocks>
|
||||
|
||||
|
@ -23,8 +23,6 @@
|
||||
.position-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
/* top: calc(50% - 60px); */
|
||||
top: 180px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
@ -48,3 +46,11 @@
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-block {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
top: 80px;
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import { Component, OnInit, OnDestroy, Renderer2 } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
@ -11,6 +9,8 @@ import { StateService } from 'src/app/services/state.service';
|
||||
styleUrls: ['./blockchain.component.scss']
|
||||
})
|
||||
export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
@Input() position: 'middle' | 'top' = 'middle';
|
||||
|
||||
txTrackingSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
|
||||
@ -19,59 +19,10 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
isLoading = true;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
/*
|
||||
this.apiService.webSocketWant(['stats', 'blocks', 'mempool-blocks']);
|
||||
|
||||
this.txTrackingSubscription = this.memPoolService.txTracking$
|
||||
.subscribe((response: ITxTracking) => {
|
||||
this.txTrackingLoading = false;
|
||||
this.txShowTxNotFound = response.notFound;
|
||||
if (this.txShowTxNotFound) {
|
||||
setTimeout(() => { this.txShowTxNotFound = false; }, 2000);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
/*
|
||||
this.route.paramMap
|
||||
.subscribe((params: ParamMap) => {
|
||||
if (this.memPoolService.txTracking$.value.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const txId: string | null = params.get('id');
|
||||
if (!txId) {
|
||||
return;
|
||||
}
|
||||
this.txTrackingLoading = true;
|
||||
this.apiService.webSocketStartTrackTx(txId);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
this.memPoolService.txIdSearch$
|
||||
.subscribe((txId) => {
|
||||
if (txId) {
|
||||
|
||||
if (this.memPoolService.txTracking$.value.enabled
|
||||
&& this.memPoolService.txTracking$.value.tx
|
||||
&& this.memPoolService.txTracking$.value.tx.txid === txId) {
|
||||
return;
|
||||
}
|
||||
console.log('enabling tracking loading from idSearch!');
|
||||
this.txTrackingLoading = true;
|
||||
this.websocketService.startTrackTx(txId);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.pipe(
|
||||
take(1)
|
||||
@ -81,6 +32,5 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
// this.txTrackingSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<span #buttonWrapper [attr.data-tlite]="'Copied!'">
|
||||
<span #buttonWrapper [attr.data-tlite]="'Copied!'" style="position: relative;">
|
||||
<button #btn class="btn btn-sm btn-link pt-0" style="line-height: 1;" [attr.data-clipboard-text]="text">
|
||||
<img src="./assets/clippy.svg" width="13">
|
||||
</button>
|
||||
|
20
frontend/src/app/components/explorer/explorer.component.html
Normal file
20
frontend/src/app/components/explorer/explorer.component.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="container">
|
||||
|
||||
<br>
|
||||
|
||||
<ul class="nav nav-tabs mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [class.active]="view === 'blocks'" routerLink="/explorer" (click)="view = 'blocks'">Blocks</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [class.active]="view === 'transactions'" routerLink="/explorer" fragment="transactions" (click)="view = 'transactions'">Transactions</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<app-latest-blocks *ngIf="view === 'blocks'; else latestTransactions"></app-latest-blocks>
|
||||
<ng-template #latestTransactions>
|
||||
<app-latest-transactions></app-latest-transactions>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<br>
|
@ -0,0 +1,3 @@
|
||||
.search-container {
|
||||
padding-top: 50px;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExplorerComponent } from './explorer.component';
|
||||
|
||||
describe('ExplorerComponent', () => {
|
||||
let component: ExplorerComponent;
|
||||
let fixture: ComponentFixture<ExplorerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ExplorerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExplorerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
25
frontend/src/app/components/explorer/explorer.component.ts
Normal file
25
frontend/src/app/components/explorer/explorer.component.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-explorer',
|
||||
templateUrl: './explorer.component.html',
|
||||
styleUrls: ['./explorer.component.scss']
|
||||
})
|
||||
export class ExplorerComponent implements OnInit {
|
||||
view: 'blocks' | 'transactions' = 'blocks';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.fragment
|
||||
.subscribe((fragment: string) => {
|
||||
if (fragment === 'transactions' ) {
|
||||
this.view = 'transactions';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
18
frontend/src/app/components/footer/footer.component.html
Normal file
18
frontend/src/app/components/footer/footer.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="my-2 my-md-0 mr-md-3">
|
||||
<div *ngIf="memPoolInfo" class="info-block">
|
||||
<span class="unconfirmedTx">Unconfirmed transactions:</span> <b>{{ memPoolInfo?.memPoolInfo?.size | number }}</b>
|
||||
<br />
|
||||
<span class="mempoolSize">Mempool size:</span> <b>{{ mempoolSize | bytes }} ({{ mempoolBlocks }} block<span [hidden]="mempoolBlocks <= 1">s</span>)</b>
|
||||
<br />
|
||||
<span class="txPerSecond">Tx weight per second:</span>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{ progressClass }}" role="progressbar" [ngStyle]="{'width': progressWidth}">{{ memPoolInfo?.vBytesPerSecond | ceil | number }} vBytes/s</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
44
frontend/src/app/components/footer/footer.component.scss
Normal file
44
frontend/src/app/components/footer/footer.component.scss
Normal file
@ -0,0 +1,44 @@
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.footer > .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.txPerSecond {
|
||||
color: #4a9ff4;
|
||||
}
|
||||
|
||||
.mempoolSize {
|
||||
color: #4a68b9;
|
||||
}
|
||||
|
||||
.unconfirmedTx {
|
||||
color: #f14d80;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
float: left;
|
||||
width: 350px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 160px;
|
||||
background-color: #2d3348;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #b58800 !important;
|
||||
}
|
61
frontend/src/app/components/footer/footer.component.ts
Normal file
61
frontend/src/app/components/footer/footer.component.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { MemPoolState } from 'src/app/interfaces/websocket.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
memPoolInfo: MemPoolState | undefined;
|
||||
mempoolBlocks = 0;
|
||||
progressWidth = '';
|
||||
progressClass: string;
|
||||
mempoolSize = 0;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.mempoolStats$
|
||||
.subscribe((mempoolState) => {
|
||||
this.memPoolInfo = mempoolState;
|
||||
this.updateProgress();
|
||||
});
|
||||
|
||||
this.stateService.mempoolBlocks$
|
||||
.subscribe((mempoolBlocks) => {
|
||||
if (!mempoolBlocks.length) { return; }
|
||||
const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b);
|
||||
const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b);
|
||||
this.mempoolSize = size;
|
||||
this.mempoolBlocks = Math.ceil(vsize / 1000000);
|
||||
});
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
if (!this.memPoolInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vBytesPerSecondLimit = 1667;
|
||||
|
||||
let vBytesPerSecond = this.memPoolInfo.vBytesPerSecond;
|
||||
if (vBytesPerSecond > 1667) {
|
||||
vBytesPerSecond = 1667;
|
||||
}
|
||||
|
||||
const percent = Math.round((vBytesPerSecond / vBytesPerSecondLimit) * 100);
|
||||
this.progressWidth = percent + '%';
|
||||
|
||||
if (percent <= 75) {
|
||||
this.progressClass = 'bg-success';
|
||||
} else if (percent <= 99) {
|
||||
this.progressClass = 'bg-warning';
|
||||
} else {
|
||||
this.progressClass = 'bg-danger';
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||
<td><a [routerLink]="['./block', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a></td>
|
||||
<td><a [routerLink]="['/block', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a></td>
|
||||
<td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td>{{ block.timestamp | timeSince : trigger }} ago</td>
|
||||
<td>{{ block.tx_count }}</td>
|
||||
@ -34,10 +34,6 @@
|
||||
</table>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoading">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</ng-template>
|
||||
<br>
|
||||
<button *ngIf="blocks.length" [disabled]="isLoading" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<ng-container *ngIf="(transactions$ | async) as transactions">
|
||||
<ng-template [ngIf]="!isLoading">
|
||||
<tr *ngFor="let transaction of transactions">
|
||||
<td><a [routerLink]="['./tx/', transaction.txid]">{{ transaction.txid }}</a></td>
|
||||
<td><a [routerLink]="['/tx/', transaction.txid]">{{ transaction.txid }}</a></td>
|
||||
<td>{{ transaction.value / 100000000 }} BTC</td>
|
||||
<td>{{ transaction.vsize | vbytes: 2 }}</td>
|
||||
<td>{{ transaction.fee / transaction.vsize | number : '1.2-2'}} sats/vB</td>
|
||||
|
@ -9,7 +9,10 @@
|
||||
<div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
||||
<a class="nav-link" routerLink="/" (click)="collapse()">Explorer</a>
|
||||
<a class="nav-link" routerLink="/" (click)="collapse()">Blockchain</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="/graphs" (click)="collapse()">Graphs</a>
|
||||
|
@ -1,21 +1,4 @@
|
||||
<ng-template [ngIf]="location === 'start'" [ngIfElse]="top">
|
||||
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="form-row">
|
||||
<div class="col-12 col-md-10 mb-2 mb-md-0">
|
||||
<input formControlName="searchText" type="text" class="form-control form-control-lg" [placeholder]="searchBoxPlaceholderText">
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<button type="submit" class="btn btn-block btn-lg btn-primary">{{ searchButtonText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #top>
|
||||
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="form-row">
|
||||
<div style="width: 350px;" class="mr-2">
|
||||
<input formControlName="searchText" type="text" class="form-control" [placeholder]="searchBoxPlaceholderText">
|
||||
@ -24,6 +7,4 @@
|
||||
<button type="submit" class="btn btn-block btn-primary">{{ searchButtonText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</ng-template>
|
||||
</form>
|
@ -9,7 +9,6 @@ import { Router } from '@angular/router';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() location: string;
|
||||
searchForm: FormGroup;
|
||||
|
||||
searchButtonText = 'Search';
|
||||
|
@ -1,20 +1,3 @@
|
||||
<app-blockchain></app-blockchain>
|
||||
|
||||
<div class="box">
|
||||
<div class="container">
|
||||
|
||||
<ul class="nav nav-tabs mb-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [class.active]="view === 'blocks'" href="#" (click)="view = 'blocks'">Blocks</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [class.active]="view === 'transactions'" href="#" (click)="view = 'transactions'">Transactions</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<app-latest-blocks *ngIf="view === 'blocks'; else latestTransactions"></app-latest-blocks>
|
||||
<ng-template #latestTransactions>
|
||||
<app-latest-transactions></app-latest-transactions>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
@ -1,10 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-start',
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss']
|
||||
})
|
||||
export class StartComponent {
|
||||
export class StartComponent implements OnInit {
|
||||
view: 'blocks' | 'transactions' = 'blocks';
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||
}
|
||||
}
|
||||
|
@ -143,10 +143,10 @@ export class StatisticsComponent implements OnInit {
|
||||
switchMap(() => {
|
||||
this.spinnerLoading = true;
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
||||
this.websocketService.want(['live-2h-chart']);
|
||||
this.websocketService.want(['blocks', 'live-2h-chart']);
|
||||
return this.apiService.list2HStatistics$();
|
||||
}
|
||||
this.websocketService.want([]);
|
||||
this.websocketService.want(['blocks']);
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
||||
return this.apiService.list24HStatistics$();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#tv-wrapper {
|
||||
height: 100%;
|
||||
margin: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.blockchain-wrapper {
|
||||
|
@ -24,15 +24,12 @@ export class TelevisionComponent implements OnInit {
|
||||
private websocketService: WebsocketService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private vbytesPipe: VbytesPipe,
|
||||
private renderer: Renderer2,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['live-2h-chart']);
|
||||
|
||||
this.renderer.addClass(document.body, 'disable-scroll');
|
||||
this.websocketService.want(['blocks', 'live-2h-chart']);
|
||||
|
||||
const labelInterpolationFnc = (value: any, index: any) => {
|
||||
return index % 6 === 0 ? formatDate(value, 'HH:mm', this.locale) : null;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="container">
|
||||
|
||||
<app-blockchain></app-blockchain>
|
||||
<app-blockchain position="top"></app-blockchain>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
<td>Included in block</td>
|
||||
<td><a [routerLink]="['/block/', tx.status.block_hash]" [state]="{ data: { blockHeight: tx.status.block_height } }">#{{ tx.status.block_height }}</a> at {{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} <i>(<app-time-since [time]="tx.status.block_time"></app-time-since> ago)</i></td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="tx.fee">
|
||||
<tr>
|
||||
<td>Fees</td>
|
||||
<td>{{ tx.fee | number }} sats <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>)</span></td>
|
||||
@ -34,6 +35,7 @@
|
||||
<td>Fees per vByte</td>
|
||||
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -66,15 +68,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
<!--
|
||||
<div>
|
||||
<app-mempool-blocks style="right: 20px; position: relative;" *ngIf="!tx.status.confirmed" [txFeePerVSize]="tx.fee / (tx.weight / 4)" (rightPosition)="rightPosition = $event" (blockDepth)="blockDepth = $event"></app-mempool-blocks>
|
||||
</div>
|
||||
-->
|
||||
<div class="clearfix"></div>
|
||||
</ng-template>
|
||||
|
||||
<h2>Inputs & Outputs</h2>
|
||||
|
||||
|
@ -31,6 +31,8 @@ export class TransactionComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.txId = params.get('id') || '';
|
||||
|
@ -1,6 +1,7 @@
|
||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
||||
<div *ngIf="!transactionPage" class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/', tx.txid]" [state]="{ data: tx }">{{ tx.txid }}</a>
|
||||
<div class="float-right">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</div>
|
||||
</div>
|
||||
<div class="header-bg box">
|
||||
<div class="row">
|
||||
@ -9,14 +10,19 @@
|
||||
<tbody>
|
||||
<tr *ngFor="let vin of tx.vin">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null" [ngIfElse]="hasPrevout">
|
||||
<i class="arrow grey"></i>
|
||||
</ng-template>
|
||||
<ng-template #hasPrevout>
|
||||
<a [routerLink]="['/tx/', vin.txid]">
|
||||
<i class="arrow green"></i>
|
||||
</a>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
|
||||
Coinbase
|
||||
Coinbase (Newly Generated Coins)
|
||||
</ng-template>
|
||||
<ng-template #regularVin>
|
||||
<a [routerLink]="['/address/', vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">{{ vin.prevout.scriptpubkey_address | shortenString : 42 }}</a>
|
||||
@ -55,9 +61,9 @@
|
||||
<td class="pl-1 arrow-td">
|
||||
<i *ngIf="!outspends[i]; else outspend" class="arrow grey"></i>
|
||||
<ng-template #outspend>
|
||||
<i *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="arrow green"></i>
|
||||
<i *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="arrow red"></i>
|
||||
<ng-template #spent>
|
||||
<a [routerLink]="['/tx/', outspends[i][vindex].txid]"><i class="arrow red"></i></a>
|
||||
<a [routerLink]="['/tx/', outspends[i][vindex].txid]"><i class="arrow green"></i></a>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
|
@ -7,6 +7,10 @@ export interface WebsocketResponse {
|
||||
txId?: string;
|
||||
txConfirmed?: boolean;
|
||||
historicalDate?: string;
|
||||
mempoolInfo?: MempoolInfo;
|
||||
vBytesPerSecond?: number;
|
||||
action?: string;
|
||||
data?: string[];
|
||||
}
|
||||
|
||||
export interface MempoolBlock {
|
||||
@ -16,3 +20,17 @@ export interface MempoolBlock {
|
||||
medianFee: number;
|
||||
feeRange: number[];
|
||||
}
|
||||
|
||||
export interface MemPoolState {
|
||||
memPoolInfo: MempoolInfo;
|
||||
vBytesPerSecond: number;
|
||||
}
|
||||
|
||||
export interface MempoolInfo {
|
||||
size: number;
|
||||
bytes: number;
|
||||
usage?: number;
|
||||
maxmempool?: number;
|
||||
mempoolminfee?: number;
|
||||
minrelaytxfee?: number;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Block } from '../interfaces/electrs.interface';
|
||||
import { MempoolBlock } from '../interfaces/websocket.interface';
|
||||
import { MempoolBlock, MemPoolState } from '../interfaces/websocket.interface';
|
||||
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
|
||||
@Injectable({
|
||||
@ -11,6 +11,7 @@ export class StateService {
|
||||
latestBlockHeight = 0;
|
||||
blocks$ = new ReplaySubject<Block>(8);
|
||||
conversions$ = new ReplaySubject<any>(1);
|
||||
mempoolStats$ = new ReplaySubject<MemPoolState>();
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
txConfirmed = new Subject<Block>();
|
||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||
|
@ -24,12 +24,14 @@ export class WebsocketService {
|
||||
}
|
||||
|
||||
startSubscription() {
|
||||
this.websocketSubject.next({'action': 'init'});
|
||||
this.websocketSubject
|
||||
.pipe(
|
||||
retryWhen((errors: any) => errors
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.goneOffline = true;
|
||||
this.websocketSubject.next({'action': 'init'});
|
||||
this.stateService.isOffline$.next(true);
|
||||
}),
|
||||
delay(5000),
|
||||
@ -39,11 +41,17 @@ export class WebsocketService {
|
||||
.subscribe((response: WebsocketResponse) => {
|
||||
if (response.blocks && response.blocks.length) {
|
||||
const blocks = response.blocks;
|
||||
blocks.forEach((block: Block) => this.stateService.blocks$.next(block));
|
||||
blocks.forEach((block: Block) => {
|
||||
if (block.height > this.stateService.latestBlockHeight) {
|
||||
this.stateService.latestBlockHeight = block.height;
|
||||
this.stateService.blocks$.next(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (response.block) {
|
||||
if (this.stateService.latestBlockHeight < response.block.height) {
|
||||
if (response.block.height > this.stateService.latestBlockHeight) {
|
||||
this.stateService.latestBlockHeight = response.block.height;
|
||||
this.stateService.blocks$.next(response.block);
|
||||
}
|
||||
|
||||
@ -61,6 +69,17 @@ export class WebsocketService {
|
||||
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
|
||||
}
|
||||
|
||||
if (response['live-2h-chart']) {
|
||||
this.stateService.live2Chart$.next(response['live-2h-chart']);
|
||||
}
|
||||
|
||||
if (response.mempoolInfo) {
|
||||
this.stateService.mempoolStats$.next({
|
||||
memPoolInfo: response.mempoolInfo,
|
||||
vBytesPerSecond: response.vBytesPerSecond,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.goneOffline === true) {
|
||||
this.goneOffline = false;
|
||||
if (this.lastWant) {
|
||||
@ -90,8 +109,7 @@ export class WebsocketService {
|
||||
}
|
||||
|
||||
want(data: string[]) {
|
||||
// @ts-ignore
|
||||
this.websocketSubject.next({action: 'want', data});
|
||||
this.websocketSubject.next({action: 'want', data: data});
|
||||
this.lastWant = data;
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ $body-bg: #1d1f31;
|
||||
$body-color: #fff;
|
||||
$gray-800: #1d1f31;
|
||||
$gray-700: #fff;
|
||||
$gray-200: #10131f;
|
||||
|
||||
$nav-tabs-link-active-bg: #24273e;
|
||||
$nav-tabs-link-active-bg: #11131f;
|
||||
|
||||
$primary: #2b89c7;
|
||||
$secondary: #2d3348;
|
||||
|
||||
$link-color: #1bd8f4;
|
||||
$link-decoration: none !default;
|
||||
@ -20,12 +20,17 @@ $link-hover-decoration: underline !default;
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #11131f;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user