Refactor. API explanations. UX revamp.
This commit is contained in:
parent
acd658a0e7
commit
34645908e9
@ -1,5 +1,5 @@
|
|||||||
const config = require('../../../mempool-config.json');
|
const config = require('../../../mempool-config.json');
|
||||||
import { Transaction, Block } from '../../interfaces';
|
import { Transaction, Block, MempoolInfo } from '../../interfaces';
|
||||||
import * as request from 'request';
|
import * as request from 'request';
|
||||||
|
|
||||||
class ElectrsApi {
|
class ElectrsApi {
|
||||||
@ -7,6 +7,27 @@ class ElectrsApi {
|
|||||||
constructor() {
|
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'][]> {
|
getRawMempool(): Promise<Transaction['txid'][]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000, forever: true }, (err, res, response) => {
|
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 {
|
public getMempoolInfo(): MempoolInfo | undefined {
|
||||||
return this.mempoolInfo;
|
return this.mempoolInfo;
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async runMempoolIntervalFunctions() {
|
private async runMempoolIntervalFunctions() {
|
||||||
|
await memPool.updateMemPoolInfo();
|
||||||
await blocks.updateBlocks();
|
await blocks.updateBlocks();
|
||||||
await memPool.updateMempool();
|
await memPool.updateMempool();
|
||||||
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.ELECTRS_POLL_RATE_MS);
|
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.ELECTRS_POLL_RATE_MS);
|
||||||
@ -83,28 +84,33 @@ class Server {
|
|||||||
const parsedMessage = JSON.parse(message);
|
const parsedMessage = JSON.parse(message);
|
||||||
|
|
||||||
if (parsedMessage.action === 'want') {
|
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-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)) {
|
if (parsedMessage && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
|
||||||
client['txId'] = parsedMessage.txId;
|
client['txId'] = parsedMessage.txId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const _blocks = blocks.getBlocks();
|
|
||||||
if (!_blocks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client.send(JSON.stringify({
|
|
||||||
'blocks': _blocks,
|
|
||||||
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
|
||||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
statistics.setNewStatisticsEntryCallback((stats: Statistic) => {
|
statistics.setNewStatisticsEntryCallback((stats: Statistic) => {
|
||||||
@ -113,11 +119,13 @@ class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client['want-live-2h-chart']) {
|
if (!client['want-live-2h-chart']) {
|
||||||
client.send(JSON.stringify({
|
return;
|
||||||
'live-2h-chart': stats
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
'live-2h-chart': stats
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,6 +135,10 @@ class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!client['want-blocks']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (client['txId'] && txIds.indexOf(client['txId']) > -1) {
|
if (client['txId'] && txIds.indexOf(client['txId']) > -1) {
|
||||||
client['txId'] = null;
|
client['txId'] = null;
|
||||||
client.send(JSON.stringify({
|
client.send(JSON.stringify({
|
||||||
@ -143,16 +155,29 @@ class Server {
|
|||||||
|
|
||||||
memPool.setMempoolChangedCallback((newMempool: { [txid: string]: SimpleTransaction }) => {
|
memPool.setMempoolChangedCallback((newMempool: { [txid: string]: SimpleTransaction }) => {
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
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) => {
|
this.wss.clients.forEach((client: WebSocket) => {
|
||||||
if (client.readyState !== WebSocket.OPEN) {
|
if (client.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.send(JSON.stringify({
|
const response = {};
|
||||||
'mempool-blocks': pBlocks
|
|
||||||
}));
|
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 { AboutComponent } from './components/about/about.component';
|
||||||
import { TelevisionComponent } from './components/television/television.component';
|
import { TelevisionComponent } from './components/television/television.component';
|
||||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||||
|
import { ExplorerComponent } from './components/explorer/explorer.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -18,6 +19,10 @@ const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
component: StartComponent,
|
component: StartComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'explorer',
|
||||||
|
component: ExplorerComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'graphs',
|
path: 'graphs',
|
||||||
component: StatisticsComponent,
|
component: StatisticsComponent,
|
||||||
|
@ -38,6 +38,8 @@ import { StatisticsComponent } from './components/statistics/statistics.componen
|
|||||||
import { ChartistComponent } from './components/statistics/chartist.component';
|
import { ChartistComponent } from './components/statistics/chartist.component';
|
||||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
||||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||||
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
|
import { ExplorerComponent } from './components/explorer/explorer.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -68,7 +70,9 @@ import { BlockchainComponent } from './components/blockchain/blockchain.componen
|
|||||||
LatestTransactionsComponent,
|
LatestTransactionsComponent,
|
||||||
QrcodeComponent,
|
QrcodeComponent,
|
||||||
ClipboardComponent,
|
ClipboardComponent,
|
||||||
|
ExplorerComponent,
|
||||||
ChartistComponent,
|
ChartistComponent,
|
||||||
|
FooterComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -1,34 +1,54 @@
|
|||||||
<div class="text-center">
|
<div class="container">
|
||||||
<img src="./assets/mempool-tube.png" width="63" height="63" />
|
<div class="text-center">
|
||||||
<br /><br />
|
<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>Mempool.Space is a realtime Bitcoin blockchain explorer and mempool visualizer.</p>
|
||||||
<p>Created by <a href="https://twitter.com/softbtc">@softbtc</a>
|
<p>Created by <a href="https://twitter.com/softbtc">@softbtc</a>
|
||||||
<br />Hosted by <a href="https://twitter.com/wiz">@wiz</a>
|
<br />Hosted by <a href="https://twitter.com/wiz">@wiz</a>
|
||||||
<br />Designed by <a href="https://instagram.com/markjborg">@markjborg</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>
|
</div>
|
||||||
|
|
||||||
<br />
|
<h2>HTTP API</h2>
|
||||||
|
|
||||||
<h1>Donate</h1>
|
<table class="table">
|
||||||
<img src="./assets/btc-qr-code-segwit.png" width="200" height="200" />
|
<tr>
|
||||||
<br />
|
<td style="width: 50%;">Fee API</td>
|
||||||
bc1qqrmgr60uetlmrpylhtllawyha9z5gw6hwdmk2t
|
<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>
|
</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() {
|
ngOnInit() {
|
||||||
this.websocketService.want([]);
|
this.websocketService.want(['blocks']);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container">
|
<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>
|
<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 { Block, Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block',
|
selector: 'app-block',
|
||||||
@ -25,9 +26,12 @@ export class BlockComponent implements OnInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private websocketService: WebsocketService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
|
|
||||||
this.route.paramMap.pipe(
|
this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
const blockHash: string = params.get('id') || '';
|
const blockHash: string = params.get('id') || '';
|
||||||
@ -40,6 +44,7 @@ export class BlockComponent implements OnInit {
|
|||||||
this.blockHash = blockHash;
|
this.blockHash = blockHash;
|
||||||
|
|
||||||
if (history.state.data && history.state.data.block) {
|
if (history.state.data && history.state.data.block) {
|
||||||
|
this.blockHeight = history.state.data.block.height;
|
||||||
return of(history.state.data.block);
|
return of(history.state.data.block);
|
||||||
} else {
|
} else {
|
||||||
this.isLoadingBlock = true;
|
this.isLoadingBlock = true;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<div class="blocks-container" *ngIf="blocks.length">
|
<div class="blocks-container" *ngIf="blocks.length">
|
||||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
<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">
|
<div class="block-height">
|
||||||
<a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a>
|
<a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
.bitcoin-block {
|
.bitcoin-block {
|
||||||
width: 125px;
|
width: 125px;
|
||||||
height: 125px;
|
height: 125px;
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
|
.blockLink {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mined-block {
|
.mined-block {
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
<div *ngIf="isLoading" class="text-center">
|
<div *ngIf="isLoading" class="loading-block">
|
||||||
<h3>Loading blocks...</h3>
|
<h3>Waiting for blocks...</h3>
|
||||||
<br>
|
<br>
|
||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</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="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-mempool-blocks></app-mempool-blocks>
|
||||||
<app-blockchain-blocks></app-blockchain-blocks>
|
<app-blockchain-blocks></app-blockchain-blocks>
|
||||||
|
|
||||||
<div id="divider" *ngIf="!isLoading"></div>
|
<div id="divider" *ngIf="!isLoading"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -23,8 +23,6 @@
|
|||||||
.position-container {
|
.position-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
/* top: calc(50% - 60px); */
|
|
||||||
top: 180px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
@ -48,3 +46,11 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
position: relative;
|
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 { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,6 +9,8 @@ import { StateService } from 'src/app/services/state.service';
|
|||||||
styleUrls: ['./blockchain.component.scss']
|
styleUrls: ['./blockchain.component.scss']
|
||||||
})
|
})
|
||||||
export class BlockchainComponent implements OnInit, OnDestroy {
|
export class BlockchainComponent implements OnInit, OnDestroy {
|
||||||
|
@Input() position: 'middle' | 'top' = 'middle';
|
||||||
|
|
||||||
txTrackingSubscription: Subscription;
|
txTrackingSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
|
|
||||||
@ -19,59 +19,10 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
|
||||||
private websocketService: WebsocketService,
|
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
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$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
take(1)
|
take(1)
|
||||||
@ -81,6 +32,5 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.blocksSubscription.unsubscribe();
|
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">
|
<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">
|
<img src="./assets/clippy.svg" width="13">
|
||||||
</button>
|
</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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
<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 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.timestamp | timeSince : trigger }} ago</td>
|
||||||
<td>{{ block.tx_count }}</td>
|
<td>{{ block.tx_count }}</td>
|
||||||
@ -34,10 +34,6 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<ng-template [ngIf]="isLoading">
|
|
||||||
<div class="spinner-border"></div>
|
|
||||||
<br><br>
|
|
||||||
</ng-template>
|
|
||||||
<br>
|
<br>
|
||||||
<button *ngIf="blocks.length" [disabled]="isLoading" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
<button *ngIf="blocks.length" [disabled]="isLoading" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<ng-container *ngIf="(transactions$ | async) as transactions">
|
<ng-container *ngIf="(transactions$ | async) as transactions">
|
||||||
<ng-template [ngIf]="!isLoading">
|
<ng-template [ngIf]="!isLoading">
|
||||||
<tr *ngFor="let transaction of transactions">
|
<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.value / 100000000 }} BTC</td>
|
||||||
<td>{{ transaction.vsize | vbytes: 2 }}</td>
|
<td>{{ transaction.vsize | vbytes: 2 }}</td>
|
||||||
<td>{{ transaction.fee / transaction.vsize | number : '1.2-2'}} sats/vB</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}">
|
<div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
|
<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>
|
||||||
<li class="nav-item" routerLinkActive="active">
|
<li class="nav-item" routerLinkActive="active">
|
||||||
<a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
|
<a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
<ng-template [ngIf]="location === 'start'" [ngIfElse]="top">
|
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||||
|
<div class="form-row">
|
||||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
<div style="width: 350px;" class="mr-2">
|
||||||
<div class="form-row">
|
<input formControlName="searchText" type="text" class="form-control" [placeholder]="searchBoxPlaceholderText">
|
||||||
<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>
|
</div>
|
||||||
</form>
|
<div>
|
||||||
|
<button type="submit" class="btn btn-block btn-primary">{{ searchButtonText }}</button>
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #top>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="btn btn-block btn-primary">{{ searchButtonText }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
</ng-template>
|
|
@ -9,7 +9,6 @@ import { Router } from '@angular/router';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class SearchFormComponent implements OnInit {
|
export class SearchFormComponent implements OnInit {
|
||||||
@Input() location: string;
|
|
||||||
searchForm: FormGroup;
|
searchForm: FormGroup;
|
||||||
|
|
||||||
searchButtonText = 'Search';
|
searchButtonText = 'Search';
|
||||||
|
@ -1,20 +1,3 @@
|
|||||||
<app-blockchain></app-blockchain>
|
<app-blockchain></app-blockchain>
|
||||||
|
|
||||||
<div class="box">
|
<app-footer></app-footer>
|
||||||
<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>
|
|
@ -1,10 +1,19 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-start',
|
selector: 'app-start',
|
||||||
templateUrl: './start.component.html',
|
templateUrl: './start.component.html',
|
||||||
styleUrls: ['./start.component.scss']
|
styleUrls: ['./start.component.scss']
|
||||||
})
|
})
|
||||||
export class StartComponent {
|
export class StartComponent implements OnInit {
|
||||||
view: 'blocks' | 'transactions' = 'blocks';
|
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(() => {
|
switchMap(() => {
|
||||||
this.spinnerLoading = true;
|
this.spinnerLoading = true;
|
||||||
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
||||||
this.websocketService.want(['live-2h-chart']);
|
this.websocketService.want(['blocks', 'live-2h-chart']);
|
||||||
return this.apiService.list2HStatistics$();
|
return this.apiService.list2HStatistics$();
|
||||||
}
|
}
|
||||||
this.websocketService.want([]);
|
this.websocketService.want(['blocks']);
|
||||||
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
||||||
return this.apiService.list24HStatistics$();
|
return this.apiService.list24HStatistics$();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#tv-wrapper {
|
#tv-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 10px;
|
padding: 10px;
|
||||||
margin-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockchain-wrapper {
|
.blockchain-wrapper {
|
||||||
|
@ -24,15 +24,12 @@ export class TelevisionComponent implements OnInit {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
@Inject(LOCALE_ID) private locale: string,
|
||||||
private vbytesPipe: VbytesPipe,
|
private vbytesPipe: VbytesPipe,
|
||||||
private renderer: Renderer2,
|
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.websocketService.want(['live-2h-chart']);
|
this.websocketService.want(['blocks', 'live-2h-chart']);
|
||||||
|
|
||||||
this.renderer.addClass(document.body, 'disable-scroll');
|
|
||||||
|
|
||||||
const labelInterpolationFnc = (value: any, index: any) => {
|
const labelInterpolationFnc = (value: any, index: any) => {
|
||||||
return index % 6 === 0 ? formatDate(value, 'HH:mm', this.locale) : null;
|
return index % 6 === 0 ? formatDate(value, 'HH:mm', this.locale) : null;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<app-blockchain></app-blockchain>
|
<app-blockchain position="top"></app-blockchain>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
@ -26,14 +26,16 @@
|
|||||||
<td>Included in block</td>
|
<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>
|
<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>
|
</tr>
|
||||||
<tr>
|
<ng-template [ngIf]="tx.fee">
|
||||||
<td>Fees</td>
|
<tr>
|
||||||
<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>
|
<td>Fees</td>
|
||||||
</tr>
|
<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>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Fees per vByte</td>
|
<tr>
|
||||||
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
|
<td>Fees per vByte</td>
|
||||||
</tr>
|
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -66,16 +68,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
</ng-template>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<h2>Inputs & Outputs</h2>
|
<h2>Inputs & Outputs</h2>
|
||||||
|
|
||||||
<app-transactions-list [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>
|
<app-transactions-list [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>
|
||||||
|
@ -31,6 +31,8 @@ export class TransactionComponent implements OnInit {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
|
|
||||||
this.route.paramMap.pipe(
|
this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.txId = params.get('id') || '';
|
this.txId = params.get('id') || '';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
<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;">
|
<div *ngIf="!transactionPage" class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||||
<a [routerLink]="['/tx/', tx.txid]" [state]="{ data: tx }">{{ tx.txid }}</a>
|
<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>
|
||||||
<div class="header-bg box">
|
<div class="header-bg box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -9,14 +10,19 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let vin of tx.vin">
|
<tr *ngFor="let vin of tx.vin">
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<a [routerLink]="['/tx/', vin.txid]">
|
<ng-template [ngIf]="vin.prevout === null" [ngIfElse]="hasPrevout">
|
||||||
<i class="arrow green"></i>
|
<i class="arrow grey"></i>
|
||||||
</a>
|
</ng-template>
|
||||||
|
<ng-template #hasPrevout>
|
||||||
|
<a [routerLink]="['/tx/', vin.txid]">
|
||||||
|
<i class="arrow green"></i>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
|
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
|
||||||
Coinbase
|
Coinbase (Newly Generated Coins)
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #regularVin>
|
<ng-template #regularVin>
|
||||||
<a [routerLink]="['/address/', vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">{{ vin.prevout.scriptpubkey_address | shortenString : 42 }}</a>
|
<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">
|
<td class="pl-1 arrow-td">
|
||||||
<i *ngIf="!outspends[i]; else outspend" class="arrow grey"></i>
|
<i *ngIf="!outspends[i]; else outspend" class="arrow grey"></i>
|
||||||
<ng-template #outspend>
|
<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>
|
<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>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
|
@ -7,6 +7,10 @@ export interface WebsocketResponse {
|
|||||||
txId?: string;
|
txId?: string;
|
||||||
txConfirmed?: boolean;
|
txConfirmed?: boolean;
|
||||||
historicalDate?: string;
|
historicalDate?: string;
|
||||||
|
mempoolInfo?: MempoolInfo;
|
||||||
|
vBytesPerSecond?: number;
|
||||||
|
action?: string;
|
||||||
|
data?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MempoolBlock {
|
export interface MempoolBlock {
|
||||||
@ -16,3 +20,17 @@ export interface MempoolBlock {
|
|||||||
medianFee: number;
|
medianFee: number;
|
||||||
feeRange: 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 { Injectable } from '@angular/core';
|
||||||
import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs';
|
import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs';
|
||||||
import { Block } from '../interfaces/electrs.interface';
|
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';
|
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -11,6 +11,7 @@ export class StateService {
|
|||||||
latestBlockHeight = 0;
|
latestBlockHeight = 0;
|
||||||
blocks$ = new ReplaySubject<Block>(8);
|
blocks$ = new ReplaySubject<Block>(8);
|
||||||
conversions$ = new ReplaySubject<any>(1);
|
conversions$ = new ReplaySubject<any>(1);
|
||||||
|
mempoolStats$ = new ReplaySubject<MemPoolState>();
|
||||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||||
txConfirmed = new Subject<Block>();
|
txConfirmed = new Subject<Block>();
|
||||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||||
|
@ -24,12 +24,14 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startSubscription() {
|
startSubscription() {
|
||||||
|
this.websocketSubject.next({'action': 'init'});
|
||||||
this.websocketSubject
|
this.websocketSubject
|
||||||
.pipe(
|
.pipe(
|
||||||
retryWhen((errors: any) => errors
|
retryWhen((errors: any) => errors
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.goneOffline = true;
|
this.goneOffline = true;
|
||||||
|
this.websocketSubject.next({'action': 'init'});
|
||||||
this.stateService.isOffline$.next(true);
|
this.stateService.isOffline$.next(true);
|
||||||
}),
|
}),
|
||||||
delay(5000),
|
delay(5000),
|
||||||
@ -39,11 +41,17 @@ export class WebsocketService {
|
|||||||
.subscribe((response: WebsocketResponse) => {
|
.subscribe((response: WebsocketResponse) => {
|
||||||
if (response.blocks && response.blocks.length) {
|
if (response.blocks && response.blocks.length) {
|
||||||
const blocks = response.blocks;
|
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 (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);
|
this.stateService.blocks$.next(response.block);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +69,17 @@ export class WebsocketService {
|
|||||||
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
|
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) {
|
if (this.goneOffline === true) {
|
||||||
this.goneOffline = false;
|
this.goneOffline = false;
|
||||||
if (this.lastWant) {
|
if (this.lastWant) {
|
||||||
@ -90,8 +109,7 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want(data: string[]) {
|
want(data: string[]) {
|
||||||
// @ts-ignore
|
this.websocketSubject.next({action: 'want', data: data});
|
||||||
this.websocketSubject.next({action: 'want', data});
|
|
||||||
this.lastWant = data;
|
this.lastWant = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ $body-bg: #1d1f31;
|
|||||||
$body-color: #fff;
|
$body-color: #fff;
|
||||||
$gray-800: #1d1f31;
|
$gray-800: #1d1f31;
|
||||||
$gray-700: #fff;
|
$gray-700: #fff;
|
||||||
$gray-200: #10131f;
|
|
||||||
|
|
||||||
$nav-tabs-link-active-bg: #24273e;
|
$nav-tabs-link-active-bg: #11131f;
|
||||||
|
|
||||||
$primary: #2b89c7;
|
$primary: #2b89c7;
|
||||||
|
$secondary: #2d3348;
|
||||||
|
|
||||||
$link-color: #1bd8f4;
|
$link-color: #1bd8f4;
|
||||||
$link-decoration: none !default;
|
$link-decoration: none !default;
|
||||||
@ -20,12 +20,17 @@ $link-hover-decoration: underline !default;
|
|||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #11131f;
|
background-color: #11131f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user