New base code for mempool blockchain explorerer
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { BlockchainComponent } from './blockchain/blockchain.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { TelevisionComponent } from './television/television.component';
|
||||
import { MasterPageComponent } from './master-page/master-page.component';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -13,30 +16,30 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
children: [],
|
||||
component: BlockchainComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
children: [],
|
||||
component: BlockchainComponent
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
children: [],
|
||||
component: AboutComponent
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
component: StatisticsComponent,
|
||||
component: StartComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'explorer',
|
||||
loadChildren: './explorer/explorer.module#ExplorerModule',
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
children: [],
|
||||
component: TransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
children: [],
|
||||
component: BlockComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -49,6 +52,7 @@ const routes: Routes = [
|
||||
redirectTo: ''
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<router-outlet></router-outlet>
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -1,56 +1,88 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { BlockchainComponent } from './blockchain/blockchain.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { MemPoolService } from './services/mem-pool.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { TxBubbleComponent } from './tx-bubble/tx-bubble.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component';
|
||||
import { TelevisionComponent } from './television/television.component';
|
||||
import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component';
|
||||
import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component';
|
||||
import { ApiService } from './services/api.service';
|
||||
import { MasterPageComponent } from './master-page/master-page.component';
|
||||
import { FeeDistributionGraphComponent } from './fee-distribution-graph/fee-distribution-graph.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgbButtonsModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
|
||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
|
||||
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
|
||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
|
||||
import { AmountComponent } from './components/amount/amount.component';
|
||||
import { StateService } from './services/state.service';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { SearchFormComponent } from './components/search-form/search-form.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { TimeSinceComponent } from './components/time-since/time-since.component';
|
||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
|
||||
import { LatestTransactionsComponent } from './components/latest-transactions/latest-transactions.component';
|
||||
import { QrcodeComponent } from './components/qrcode/qrcode.component';
|
||||
import { ClipboardComponent } from './components/clipboard/clipboard.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
import { ChartistComponent } from './components/statistics/chartist.component';
|
||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
BlockchainComponent,
|
||||
FooterComponent,
|
||||
StatisticsComponent,
|
||||
AboutComponent,
|
||||
TxBubbleComponent,
|
||||
BlockModalComponent,
|
||||
ProjectedBlockModalComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainBlocksComponent,
|
||||
BlockchainProjectedBlocksComponent,
|
||||
MasterPageComponent,
|
||||
FeeDistributionGraphComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainComponent,
|
||||
StartComponent,
|
||||
BlockchainBlocksComponent,
|
||||
StatisticsComponent,
|
||||
TransactionComponent,
|
||||
BlockComponent,
|
||||
TransactionsListComponent,
|
||||
TimeSincePipe,
|
||||
BytesPipe,
|
||||
VbytesPipe,
|
||||
WuBytesPipe,
|
||||
CeilPipe,
|
||||
ShortenStringPipe,
|
||||
AddressComponent,
|
||||
AmountComponent,
|
||||
SearchFormComponent,
|
||||
LatestBlocksComponent,
|
||||
TimeSinceComponent,
|
||||
AddressLabelsComponent,
|
||||
MempoolBlocksComponent,
|
||||
LatestTransactionsComponent,
|
||||
QrcodeComponent,
|
||||
ClipboardComponent,
|
||||
ChartistComponent,
|
||||
],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
SharedModule,
|
||||
HttpClientModule,
|
||||
ReactiveFormsModule,
|
||||
BrowserAnimationsModule,
|
||||
NgbButtonsModule,
|
||||
],
|
||||
providers: [
|
||||
ApiService,
|
||||
MemPoolService,
|
||||
],
|
||||
entryComponents: [
|
||||
BlockModalComponent,
|
||||
ProjectedBlockModalComponent,
|
||||
ElectrsApiService,
|
||||
StateService,
|
||||
WebsocketService,
|
||||
VbytesPipe,
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Fee distribution for block
|
||||
<a *ngIf="!isElectrsEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
|
||||
<a *ngIf="isElectrsEnabled" (click)="activeModal.dismiss()" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
|
||||
</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<table class="table table-borderless table-sm">
|
||||
<tr>
|
||||
<th>Median fee:</th>
|
||||
<td>~{{ block.medianFee | ceil }} sat/vB <span *ngIf="conversions">(~<span class="green-color">{{ conversions.USD * (block.medianFee/100000000)*250 | currency:'USD':'symbol':'1.2-2' }}</span>/tx)</span></td>
|
||||
<th>Block size:</th>
|
||||
<td>{{ block.size | bytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Fee span:</th>
|
||||
<td class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</td>
|
||||
<th>Tx count:</th>
|
||||
<td>{{ block.nTx }} transactions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total fees:</th>
|
||||
<td>{{ (block.fees - blockSubsidy) | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * (block.fees - blockSubsidy) | currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
|
||||
<th>Block reward + fees:</th>
|
||||
<td>{{ block.fees | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * block.fees | currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<app-fee-distribution-graph [blockHeight]="block.height"></app-fee-distribution-graph>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { IBlock } from '../../blockchain/interfaces';
|
||||
import { MemPoolService } from '../../services/mem-pool.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-modal',
|
||||
templateUrl: './block-modal.component.html',
|
||||
styleUrls: ['./block-modal.component.scss']
|
||||
})
|
||||
export class BlockModalComponent implements OnInit {
|
||||
@Input() block: IBlock;
|
||||
blockSubsidy = 50;
|
||||
isElectrsEnabled = !!environment.electrs;
|
||||
conversions: any;
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private memPoolService: MemPoolService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.memPoolService.conversions$
|
||||
.subscribe((conversions) => {
|
||||
this.conversions = conversions;
|
||||
});
|
||||
|
||||
let halvenings = Math.floor(this.block.height / 210000);
|
||||
while (halvenings > 0) {
|
||||
this.blockSubsidy = this.blockSubsidy / 2;
|
||||
halvenings--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<div class="blocks-container" *ngIf="blocks.length">
|
||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
||||
<div (click)="openBlockModal(block);" class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
|
||||
<div class="block-height">
|
||||
<a *ngIf="!isElectrsEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
|
||||
<a *ngIf="isElectrsEnabled" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
|
||||
</div>
|
||||
<div class="block-body">
|
||||
<div class="fees">
|
||||
~{{ block.medianFee | ceil }} sat/vB
|
||||
<br/>
|
||||
<span class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</span>
|
||||
</div>
|
||||
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
||||
<div class="transaction-count">{{ block.nTx }} transactions</div>
|
||||
<br /><br />
|
||||
<div class="time-difference">{{ block.time | timeSince : trigger }} ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
<div class="projected-blocks-container">
|
||||
<div *ngFor="let projectedBlock of projectedBlocks; let i = index; trackBy: trackByProjectedFn">
|
||||
<div (click)="openProjectedBlockModal(projectedBlock, i);" class="bitcoin-block text-center projected-block" id="projected-block-{{ i }}" [ngStyle]="getStyleForProjectedBlockAtIndex(i)">
|
||||
<div class="block-body" *ngIf="projectedBlocks?.length">
|
||||
<div class="fees">
|
||||
~{{ projectedBlock.medianFee | ceil }} sat/vB
|
||||
<br/>
|
||||
<span class="yellow-color">{{ projectedBlock.minFee | ceil }} - {{ projectedBlock.maxFee | ceil }} sat/vB</span>
|
||||
</div>
|
||||
<div class="block-size">{{ projectedBlock.blockSize | bytes: 2 }}</div>
|
||||
<div class="transaction-count">{{ projectedBlock.nTx }} transactions</div>
|
||||
<div class="time-difference" *ngIf="i !== 3">In ~{{ 10 * i + 10 }} minutes</div>
|
||||
<ng-template [ngIf]="i === 3 && projectedBlocks?.length >= 4 && (projectedBlock.blockWeight / 4000000 | ceil) > 1">
|
||||
<div class="time-difference">+{{ projectedBlock.blockWeight / 4000000 | ceil }} blocks</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<span class="animated-border"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { IProjectedBlock, IBlock } from '../blockchain/interfaces';
|
||||
import { ProjectedBlockModalComponent } from './projected-block-modal/projected-block-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { MemPoolService } from '../services/mem-pool.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blockchain-projected-blocks',
|
||||
templateUrl: './blockchain-projected-blocks.component.html',
|
||||
styleUrls: ['./blockchain-projected-blocks.component.scss']
|
||||
})
|
||||
export class BlockchainProjectedBlocksComponent implements OnInit, OnDestroy {
|
||||
projectedBlocks: IProjectedBlock[];
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private modalService: NgbModal,
|
||||
private memPoolService: MemPoolService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.subscription = this.memPoolService.projectedBlocks$
|
||||
.subscribe((projectedblocks) => this.projectedBlocks = projectedblocks);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
trackByProjectedFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
openProjectedBlockModal(block: IBlock, index: number) {
|
||||
const modalRef = this.modalService.open(ProjectedBlockModalComponent, { size: 'lg' });
|
||||
modalRef.componentInstance.block = block;
|
||||
modalRef.componentInstance.index = index;
|
||||
}
|
||||
|
||||
getStyleForProjectedBlockAtIndex(index: number) {
|
||||
const greenBackgroundHeight = 100 - (this.projectedBlocks[index].blockWeight / 4000000) * 100;
|
||||
if (window.innerWidth <= 768) {
|
||||
return {
|
||||
'top': 40 + index * 155 + 'px',
|
||||
'background': `repeating-linear-gradient(#554b45, #554b45 ${greenBackgroundHeight}%,
|
||||
#bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'right': 40 + index * 155 + 'px',
|
||||
'background': `repeating-linear-gradient(#554b45, #554b45 ${greenBackgroundHeight}%,
|
||||
#bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Fee distribution for projected block</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<table class="table table-borderless table-sm">
|
||||
<tr>
|
||||
<th>Median fee:</th>
|
||||
<td>~{{ block.medianFee | ceil }} sat/vB <span *ngIf="conversions">(~<span class="green-color">{{ conversions.USD * (block.medianFee/100000000)*250 | currency:'USD':'symbol':'1.2-2' }}</span>/tx)</span></td>
|
||||
<th>Tx count:</th>
|
||||
<td>{{ block.nTx }} transactions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Fee span:</th>
|
||||
<td class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total fees:</th>
|
||||
<td>{{ block.fees | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * block.fees| currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<app-fee-distribution-graph [projectedBlockIndex]="index"></app-fee-distribution-graph>
|
||||
</div>
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { MemPoolService } from '../../services/mem-pool.service';
|
||||
import { IBlock } from 'src/app/blockchain/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-projected-block-modal',
|
||||
templateUrl: './projected-block-modal.component.html',
|
||||
styleUrls: ['./projected-block-modal.component.scss']
|
||||
})
|
||||
export class ProjectedBlockModalComponent implements OnInit {
|
||||
@Input() block: IBlock;
|
||||
@Input() index: number;
|
||||
|
||||
conversions: any;
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private memPoolService: MemPoolService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.memPoolService.conversions$
|
||||
.subscribe((conversions) => {
|
||||
this.conversions = conversions;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
export interface IMempoolInfo {
|
||||
size: number;
|
||||
bytes: number;
|
||||
usage: number;
|
||||
maxmempool: number;
|
||||
mempoolminfee: number;
|
||||
minrelaytxfee: number;
|
||||
}
|
||||
|
||||
export interface IMempoolDefaultResponse {
|
||||
mempoolInfo?: IMempoolInfo;
|
||||
blocks?: IBlock[];
|
||||
block?: IBlock;
|
||||
projectedBlocks?: IProjectedBlock[];
|
||||
'live-2h-chart'?: IMempoolStats;
|
||||
txPerSecond?: number;
|
||||
vBytesPerSecond: number;
|
||||
'track-tx'?: ITrackTx;
|
||||
conversions?: any;
|
||||
}
|
||||
|
||||
export interface ITrackTx {
|
||||
tx?: ITransaction;
|
||||
blockHeight: number;
|
||||
tracking: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface IProjectedBlock {
|
||||
blockSize: number;
|
||||
blockWeight: number;
|
||||
maxFee: number;
|
||||
maxWeightFee: number;
|
||||
medianFee: number;
|
||||
minFee: number;
|
||||
minWeightFee: number;
|
||||
nTx: number;
|
||||
hasMytx: boolean;
|
||||
}
|
||||
|
||||
export interface IStrippedBlock {
|
||||
bits: number;
|
||||
difficulty: number;
|
||||
hash: string;
|
||||
height: number;
|
||||
nTx: number;
|
||||
size: number;
|
||||
strippedsize: number;
|
||||
time: number;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface ITransaction {
|
||||
txid: string;
|
||||
hash: string;
|
||||
version: number;
|
||||
size: number;
|
||||
vsize: number;
|
||||
locktime: number;
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
hex: string;
|
||||
|
||||
fee: number;
|
||||
feePerVsize: number;
|
||||
feePerWeightUnit: number;
|
||||
}
|
||||
|
||||
export interface IBlock {
|
||||
hash: string;
|
||||
confirmations: number;
|
||||
strippedsize: number;
|
||||
size: number;
|
||||
weight: number;
|
||||
height: number;
|
||||
version: number;
|
||||
versionHex: string;
|
||||
merkleroot: string;
|
||||
tx: ITransaction[];
|
||||
time: number;
|
||||
mediantime: number;
|
||||
nonce: number;
|
||||
bits: string;
|
||||
difficulty: number;
|
||||
chainwork: string;
|
||||
nTx: number;
|
||||
previousblockhash: string;
|
||||
|
||||
minFee: number;
|
||||
maxFee: number;
|
||||
medianFee: number;
|
||||
fees: number;
|
||||
}
|
||||
|
||||
interface ScriptSig {
|
||||
asm: string;
|
||||
hex: string;
|
||||
}
|
||||
|
||||
interface Vin {
|
||||
txid: string;
|
||||
vout: number;
|
||||
scriptSig: ScriptSig;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
interface ScriptPubKey {
|
||||
asm: string;
|
||||
hex: string;
|
||||
reqSigs: number;
|
||||
type: string;
|
||||
addresses: string[];
|
||||
}
|
||||
|
||||
interface Vout {
|
||||
value: number;
|
||||
n: number;
|
||||
scriptPubKey: ScriptPubKey;
|
||||
}
|
||||
|
||||
export interface IMempoolStats {
|
||||
id: number;
|
||||
added: string;
|
||||
unconfirmed_transactions: number;
|
||||
tx_per_second: number;
|
||||
vbytes_per_second: number;
|
||||
mempool_byte_weight: number;
|
||||
fee_data: IFeeData;
|
||||
vsize_1: number;
|
||||
vsize_2: number;
|
||||
vsize_3: number;
|
||||
vsize_4: number;
|
||||
vsize_5: number;
|
||||
vsize_6: number;
|
||||
vsize_8: number;
|
||||
vsize_10: number;
|
||||
vsize_12: number;
|
||||
vsize_15: number;
|
||||
vsize_20: number;
|
||||
vsize_30: number;
|
||||
vsize_40: number;
|
||||
vsize_50: number;
|
||||
vsize_60: number;
|
||||
vsize_70: number;
|
||||
vsize_80: number;
|
||||
vsize_90: number;
|
||||
vsize_100: number;
|
||||
vsize_125: number;
|
||||
vsize_150: number;
|
||||
vsize_175: number;
|
||||
vsize_200: number;
|
||||
vsize_250: number;
|
||||
vsize_300: number;
|
||||
vsize_350: number;
|
||||
vsize_400: number;
|
||||
vsize_500: number;
|
||||
vsize_600: number;
|
||||
vsize_700: number;
|
||||
vsize_800: number;
|
||||
vsize_900: number;
|
||||
vsize_1000: number;
|
||||
vsize_1200: number;
|
||||
vsize_1400: number;
|
||||
vsize_1600: number;
|
||||
vsize_1800: number;
|
||||
vsize_2000: number;
|
||||
}
|
||||
|
||||
export interface IBlockTransaction {
|
||||
f: number;
|
||||
fpv: number;
|
||||
}
|
||||
|
||||
interface IFeeData {
|
||||
wu: { [ fee: string ]: number };
|
||||
vsize: { [ fee: string ]: number };
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
<h2>About</h2>
|
||||
|
||||
<p>Mempool.Space is a realtime Bitcoin blockchain visualizer and statistics website focused on SegWit.</p>
|
||||
<p>Created by <a href="http://t.me/softcrypto">@softcrypto</a> (Telegram). <a href="https://twitter.com/softcrypt0">@softcrypt0</a> (Twitter).
|
||||
<br />Designed by <a href="https://emeraldo.io">emeraldo.io</a>.
|
||||
<br />Hosted by <a href="https://twitter.com/wiz">@wiz</a></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>
|
||||
<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>
|
||||
@@ -19,18 +19,10 @@
|
||||
<br />
|
||||
|
||||
<h1>Donate</h1>
|
||||
<h3>Segwit native</h3>
|
||||
<img src="./assets/btc-qr-code-segwit.png" width="200" height="200" />
|
||||
<br />
|
||||
bc1qqrmgr60uetlmrpylhtllawyha9z5gw6hwdmk2t
|
||||
|
||||
<br /><br />
|
||||
<h3>Segwit compatibility</h3>
|
||||
<img src="./assets/btc-qr-code.png" width="200" height="200" />
|
||||
<br />
|
||||
3Ccig4G4u8hbExnxBJHeE5ZmxxWxvEQ65f
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<h3>PayNym</h3>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
@@ -9,11 +9,11 @@ import { ApiService } from '../services/api.service';
|
||||
export class AboutComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.apiService.webSocketWant([]);
|
||||
this.websocketService.want([]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<span *ngIf="multisig" class="badge badge-pill badge-warning">multisig {{ multisigM }} of {{ multisigN }}</span>
|
||||
@@ -0,0 +1,3 @@
|
||||
.badge {
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddressLabelsComponent } from './address-labels.component';
|
||||
|
||||
describe('AddressLabelsComponent', () => {
|
||||
let component: AddressLabelsComponent;
|
||||
let fixture: ComponentFixture<AddressLabelsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AddressLabelsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddressLabelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-labels',
|
||||
templateUrl: './address-labels.component.html',
|
||||
styleUrls: ['./address-labels.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AddressLabelsComponent implements OnInit {
|
||||
|
||||
@Input() vin: Vin;
|
||||
@Input() vout: Vout;
|
||||
|
||||
multisig = false;
|
||||
multisigM: number;
|
||||
multisigN: number;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.vin) {
|
||||
this.handleVin();
|
||||
} else if (this.vout) {
|
||||
this.handleVout();
|
||||
}
|
||||
}
|
||||
|
||||
handleVin() {
|
||||
if (this.vin.inner_witnessscript_asm && this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
|
||||
const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
|
||||
this.multisig = true;
|
||||
this.multisigM = matches[0];
|
||||
this.multisigN = matches[1];
|
||||
}
|
||||
|
||||
if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
|
||||
const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
|
||||
this.multisig = true;
|
||||
this.multisigM = matches[0];
|
||||
this.multisigN = matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
handleVout() {
|
||||
}
|
||||
|
||||
getMatches(str: string, regex: RegExp, index: number) {
|
||||
if (!index) {
|
||||
index = 1;
|
||||
}
|
||||
const matches = [];
|
||||
let match;
|
||||
while (match = regex.exec(str)) {
|
||||
matches.push(match[index]);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
}
|
||||
100
frontend/src/app/components/address/address.component.html
Normal file
100
frontend/src/app/components/address/address.component.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<div class="container">
|
||||
<h1 style="float: left;">Address</h1>
|
||||
<a [routerLink]="['/address/', addressString]" style="line-height: 55px; margin-left: 10px;">{{ addressString }}</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAddress && !error">
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Number of transactions</td>
|
||||
<td>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total received</td>
|
||||
<td>{{ (address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total sent</td>
|
||||
<td>{{ (address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="address.address"></app-qrcode>
|
||||
<!--qrcode id="qrCode" [qrdata]="address.address" [size]="128" [level]="'M'"></qrcode>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }} transactions</h2>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</ng-template>
|
||||
<button *ngIf="transactions?.length && transactions?.length !== (address.chain_stats.tx_count + address.mempool_stats.tx_count)" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAddress && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading address data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,11 +1,6 @@
|
||||
.header-bg {
|
||||
background-color:#653b9c;
|
||||
font-size: 14px;
|
||||
}
|
||||
.header-bg a {
|
||||
color: #FFF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address',
|
||||
@@ -9,17 +10,16 @@ import { switchMap } from 'rxjs/operators';
|
||||
styleUrls: ['./address.component.scss']
|
||||
})
|
||||
export class AddressComponent implements OnInit {
|
||||
address: any;
|
||||
address: Address;
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
latestBlockHeight: number;
|
||||
transactions: any[];
|
||||
transactions: Transaction[];
|
||||
isLoadingTransactions = true;
|
||||
error: any;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -27,15 +27,17 @@ export class AddressComponent implements OnInit {
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
const address: string = params.get('id') || '';
|
||||
return this.apiService.getAddress$(address);
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.addressString = params.get('id') || '';
|
||||
return this.electrsApiService.getAddress$(this.addressString);
|
||||
})
|
||||
)
|
||||
.subscribe((address) => {
|
||||
this.address = address;
|
||||
this.isLoadingAddress = false;
|
||||
window.scrollTo(0, 0);
|
||||
this.getAddressTransactions(address.address);
|
||||
this.ref.markForCheck();
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
@@ -45,7 +47,7 @@ export class AddressComponent implements OnInit {
|
||||
}
|
||||
|
||||
getAddressTransactions(address: string) {
|
||||
this.apiService.getAddressTransactions$(address)
|
||||
this.electrsApiService.getAddressTransactions$(address)
|
||||
.subscribe((transactions: any) => {
|
||||
this.transactions = transactions;
|
||||
this.isLoadingTransactions = false;
|
||||
@@ -54,7 +56,7 @@ export class AddressComponent implements OnInit {
|
||||
|
||||
loadMore() {
|
||||
this.isLoadingTransactions = true;
|
||||
this.apiService.getAddressTransactionsFromHash$(this.address.id, this.transactions[this.transactions.length - 1].txid)
|
||||
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.transactions[this.transactions.length - 1].txid)
|
||||
.subscribe((transactions) => {
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
this.isLoadingTransactions = false;
|
||||
6
frontend/src/app/components/amount/amount.component.html
Normal file
6
frontend/src/app/components/amount/amount.component.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<ng-container *ngIf="(viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span>{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-container>
|
||||
<ng-template #viewFiatVin>
|
||||
{{ satoshis / 100000000 }} BTC
|
||||
</ng-template>
|
||||
3
frontend/src/app/components/amount/amount.component.scss
Normal file
3
frontend/src/app/components/amount/amount.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.green-color {
|
||||
color: #3bcc49;
|
||||
}
|
||||
25
frontend/src/app/components/amount/amount.component.spec.ts
Normal file
25
frontend/src/app/components/amount/amount.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AmountComponent } from './amount.component';
|
||||
|
||||
describe('AmountComponent', () => {
|
||||
let component: AmountComponent;
|
||||
let fixture: ComponentFixture<AmountComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AmountComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AmountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
26
frontend/src/app/components/amount/amount.component.ts
Normal file
26
frontend/src/app/components/amount/amount.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-amount',
|
||||
templateUrl: './amount.component.html',
|
||||
styleUrls: ['./amount.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AmountComponent implements OnInit {
|
||||
conversions$: Observable<any>;
|
||||
viewFiat$: Observable<boolean>;
|
||||
|
||||
@Input() satoshis: number;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
1
frontend/src/app/components/app/app.component.html
Normal file
1
frontend/src/app/components/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
7
frontend/src/app/components/app/app.component.scss
Normal file
7
frontend/src/app/components/app/app.component.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
footer {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
}
|
||||
35
frontend/src/app/components/app/app.component.spec.ts
Normal file
35
frontend/src/app/components/app/app.component.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'mempoolspace'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('mempoolspace');
|
||||
});
|
||||
|
||||
it('should render title in a h1 tag', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to mempoolspace!');
|
||||
});
|
||||
});
|
||||
15
frontend/src/app/components/app/app.component.ts
Normal file
15
frontend/src/app/components/app/app.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(
|
||||
public router: Router,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
}
|
||||
134
frontend/src/app/components/block/block.component.html
Normal file
134
frontend/src/app/components/block/block.component.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<div class="container">
|
||||
|
||||
<app-blockchain></app-blockchain>
|
||||
|
||||
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/', blockHash]">#{{ blockHeight }}</a></ng-template></h1>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||
|
||||
<br>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Timestamp</td>
|
||||
<td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} <i>(<app-time-since [time]="block.timestamp"></app-time-since> ago)</i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number of transactions</td>
|
||||
<td>{{ block.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ block.size | bytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weight</td>
|
||||
<td>{{ block.weight | wuBytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><button *ngIf="latestBlock" class="btn btn-sm btn-success">{{ (latestBlock.height - block.height + 1) }} confirmation{{ (latestBlock.height - block.height + 1) === 1 ? '' : 's' }}</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hash</td>
|
||||
<td><a [routerLink]="['/block/', block.id]" title="{{ block.id }}" >{{ block.id | shortenString : 32 }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Previous Block</td>
|
||||
<td><a [routerLink]="['/block/', block.previousblockhash]" [state]="{ data: { blockHeight: blockHeight - 1 } }" title="{{ block.previousblockhash }}">{{ block.previousblockhash | shortenString : 32 }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ block.tx_count }} transactions</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<app-transactions-list [transactions]="transactions"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</ng-template>
|
||||
<button *ngIf="transactions?.length && transactions?.length !== block.tx_count" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingBlock && !error">
|
||||
|
||||
<br>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading block data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { MemPoolService } from 'src/app/services/mem-pool.service';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { ChangeDetectorRef } from '@angular/core';
|
||||
import { Block, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -11,48 +12,59 @@ import { ChangeDetectorRef } from '@angular/core';
|
||||
styleUrls: ['./block.component.scss']
|
||||
})
|
||||
export class BlockComponent implements OnInit {
|
||||
block: any;
|
||||
block: Block;
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
isLoadingBlock = true;
|
||||
latestBlockHeight: number;
|
||||
transactions: any[];
|
||||
latestBlock: Block;
|
||||
transactions: Transaction[];
|
||||
isLoadingTransactions = true;
|
||||
error: any;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private memPoolService: MemPoolService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingBlock = true;
|
||||
const blockHash: string = params.get('id') || '';
|
||||
return this.apiService.getBlock$(blockHash);
|
||||
this.error = undefined;
|
||||
|
||||
if (history.state.data && history.state.data.blockHeight) {
|
||||
this.blockHeight = history.state.data.blockHeight;
|
||||
}
|
||||
|
||||
this.blockHash = blockHash;
|
||||
|
||||
if (history.state.data && history.state.data.block) {
|
||||
return of(history.state.data.block);
|
||||
} else {
|
||||
this.isLoadingBlock = true;
|
||||
return this.electrsApiService.getBlock$(blockHash);
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe((block) => {
|
||||
.subscribe((block: Block) => {
|
||||
this.block = block;
|
||||
this.blockHeight = block.height;
|
||||
this.isLoadingBlock = false;
|
||||
this.getBlockTransactions(block.id);
|
||||
this.ref.markForCheck();
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
this.isLoadingBlock = false;
|
||||
});
|
||||
|
||||
this.memPoolService.blocks$
|
||||
.subscribe((block) => {
|
||||
this.latestBlockHeight = block.height;
|
||||
});
|
||||
this.stateService.blocks$
|
||||
.subscribe((block) => this.latestBlock = block);
|
||||
}
|
||||
|
||||
getBlockTransactions(hash: string) {
|
||||
this.apiService.getBlockTransactions$(hash)
|
||||
this.electrsApiService.getBlockTransactions$(hash)
|
||||
.subscribe((transactions: any) => {
|
||||
this.transactions = transactions;
|
||||
this.isLoadingTransactions = false;
|
||||
@@ -61,7 +73,7 @@ export class BlockComponent implements OnInit {
|
||||
|
||||
loadMore() {
|
||||
this.isLoadingTransactions = true;
|
||||
this.apiService.getBlockTransactions$(this.block.id, this.transactions.length)
|
||||
this.electrsApiService.getBlockTransactions$(this.block.id, this.transactions.length)
|
||||
.subscribe((transactions) => {
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
this.isLoadingTransactions = false;
|
||||
@@ -0,0 +1,20 @@
|
||||
<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="block-height">
|
||||
<a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a>
|
||||
</div>
|
||||
<div class="block-body">
|
||||
<div class="fees">
|
||||
~{{ block.medianFee | ceil }} sat/vB
|
||||
<br/>
|
||||
<span class="yellow-color">{{ block.feeRange[0] | ceil }} - {{ block.feeRange[block.feeRange.length - 1] | ceil }} sat/vB</span>
|
||||
</div>
|
||||
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
||||
<div class="transaction-count">{{ block.tx_count }} transactions</div>
|
||||
<br /><br />
|
||||
<div class="time-difference">{{ block.timestamp | timeSince : trigger }} ago</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { IBlock } from '../blockchain/interfaces';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BlockModalComponent } from './block-modal/block-modal.component';
|
||||
import { MemPoolService } from '../services/mem-pool.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Block } from 'src/app/interfaces/electrs.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blockchain-blocks',
|
||||
@@ -12,19 +9,17 @@ import { environment } from '../../environments/environment';
|
||||
styleUrls: ['./blockchain-blocks.component.scss']
|
||||
})
|
||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
blocks: IBlock[] = [];
|
||||
blocks: Block[] = [];
|
||||
blocksSubscription: Subscription;
|
||||
interval: any;
|
||||
trigger = 0;
|
||||
isElectrsEnabled = !!environment.electrs;
|
||||
|
||||
constructor(
|
||||
private modalService: NgbModal,
|
||||
private memPoolService: MemPoolService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.blocksSubscription = this.memPoolService.blocks$
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe((block) => {
|
||||
if (this.blocks.some((b) => b.height === block.height)) {
|
||||
return;
|
||||
@@ -41,27 +36,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
trackByBlocksFn(index: number, item: IBlock) {
|
||||
trackByBlocksFn(index: number, item: Block) {
|
||||
return item.height;
|
||||
}
|
||||
|
||||
openBlockModal(block: IBlock) {
|
||||
const modalRef = this.modalService.open(BlockModalComponent, { size: 'lg' });
|
||||
modalRef.componentInstance.block = block;
|
||||
}
|
||||
|
||||
getStyleForBlock(block: IBlock) {
|
||||
getStyleForBlock(block: Block) {
|
||||
const greenBackgroundHeight = 100 - (block.weight / 4000000) * 100;
|
||||
if (window.innerWidth <= 768) {
|
||||
return {
|
||||
'top': 155 * this.blocks.indexOf(block) + 'px',
|
||||
'background': `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
|
||||
top: 155 * this.blocks.indexOf(block) + 'px',
|
||||
background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
|
||||
#9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'left': 155 * this.blocks.indexOf(block) + 'px',
|
||||
'background': `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
|
||||
left: 155 * this.blocks.indexOf(block) + 'px',
|
||||
background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
|
||||
#9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`,
|
||||
};
|
||||
}
|
||||
@@ -11,14 +11,10 @@
|
||||
</div>
|
||||
<div class="text-center" class="blockchain-wrapper">
|
||||
<div class="position-container">
|
||||
<app-blockchain-projected-blocks></app-blockchain-projected-blocks>
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
<app-blockchain-blocks></app-blockchain-blocks>
|
||||
|
||||
<div id="divider" *ngIf="!isLoading"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<app-tx-bubble></app-tx-bubble>
|
||||
|
||||
<app-footer></app-footer>
|
||||
@@ -1,8 +1,8 @@
|
||||
#divider {
|
||||
width: 3px;
|
||||
height: 3000px;
|
||||
height: 200px;
|
||||
left: 0;
|
||||
top: -1000px;
|
||||
top: -50px;
|
||||
background-image: url('/assets/divider-new.png');
|
||||
background-repeat: repeat-y;
|
||||
position: absolute;
|
||||
@@ -17,12 +17,14 @@
|
||||
|
||||
.blockchain-wrapper {
|
||||
overflow: hidden;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.position-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: calc(50% - 60px);
|
||||
/* top: calc(50% - 60px); */
|
||||
top: 180px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, OnDestroy, Renderer2 } from '@angular/core';
|
||||
import { MemPoolService, ITxTracking } from '../services/mem-pool.service';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
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({
|
||||
selector: 'app-blockchain',
|
||||
@@ -19,14 +19,14 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
isLoading = true;
|
||||
|
||||
constructor(
|
||||
private memPoolService: MemPoolService,
|
||||
private apiService: ApiService,
|
||||
private renderer: Renderer2,
|
||||
private route: ActivatedRoute,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.apiService.webSocketWant(['stats', 'blocks', 'projected-blocks']);
|
||||
/*
|
||||
this.apiService.webSocketWant(['stats', 'blocks', 'mempool-blocks']);
|
||||
|
||||
this.txTrackingSubscription = this.memPoolService.txTracking$
|
||||
.subscribe((response: ITxTracking) => {
|
||||
@@ -36,9 +36,9 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
setTimeout(() => { this.txShowTxNotFound = false; }, 2000);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
this.renderer.addClass(document.body, 'disable-scroll');
|
||||
|
||||
/*
|
||||
this.route.paramMap
|
||||
.subscribe((params: ParamMap) => {
|
||||
if (this.memPoolService.txTracking$.value.enabled) {
|
||||
@@ -53,6 +53,9 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
this.apiService.webSocketStartTrackTx(txId);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
this.memPoolService.txIdSearch$
|
||||
.subscribe((txId) => {
|
||||
if (txId) {
|
||||
@@ -64,11 +67,12 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
console.log('enabling tracking loading from idSearch!');
|
||||
this.txTrackingLoading = true;
|
||||
this.apiService.webSocketStartTrackTx(txId);
|
||||
this.websocketService.startTrackTx(txId);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
this.blocksSubscription = this.memPoolService.blocks$
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.pipe(
|
||||
take(1)
|
||||
)
|
||||
@@ -77,7 +81,6 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy() {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.txTrackingSubscription.unsubscribe();
|
||||
this.renderer.removeClass(document.body, 'disable-scroll');
|
||||
// this.txTrackingSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<span #buttonWrapper [attr.data-tlite]="'Copied!'">
|
||||
<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>
|
||||
</span>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ClipboardComponent } from './clipboard.component';
|
||||
|
||||
describe('ClipboardComponent', () => {
|
||||
let component: ClipboardComponent;
|
||||
let fixture: ComponentFixture<ClipboardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ClipboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClipboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
33
frontend/src/app/components/clipboard/clipboard.component.ts
Normal file
33
frontend/src/app/components/clipboard/clipboard.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, Input } from '@angular/core';
|
||||
import * as ClipboardJS from 'clipboard';
|
||||
import * as tlite from 'tlite';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clipboard',
|
||||
templateUrl: './clipboard.component.html',
|
||||
styleUrls: ['./clipboard.component.scss']
|
||||
})
|
||||
export class ClipboardComponent implements AfterViewInit {
|
||||
@ViewChild('btn') btn: ElementRef;
|
||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
||||
@Input() text: string;
|
||||
|
||||
clipboard: any;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.clipboard = new ClipboardJS(this.btn.nativeElement);
|
||||
this.clipboard.on('success', (e) => {
|
||||
tlite.show(this.buttonWrapper.nativeElement);
|
||||
setTimeout(() => {
|
||||
tlite.hide(this.buttonWrapper.nativeElement);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th style="width: 120px;">Height</th>
|
||||
<th class="d-none d-md-block" style="width: 300px;">Timestamp</th>
|
||||
<th style="width: 200px;">Mined</th>
|
||||
<th style="width: 150px;">Transactions</th>
|
||||
<th style="width: 175px;">Size</th>
|
||||
<th class="d-none d-md-block">Filled</th>
|
||||
</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 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>
|
||||
<td>{{ block.size | bytes: 2 }}</td>
|
||||
<td class="d-none d-md-block">
|
||||
<div class="progress position-relative">
|
||||
<div class="progress-bar bg-success" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="isLoading">
|
||||
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</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>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LatestBlocksComponent } from './latest-blocks.component';
|
||||
|
||||
describe('LatestBlocksComponent', () => {
|
||||
let component: LatestBlocksComponent;
|
||||
let fixture: ComponentFixture<LatestBlocksComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LatestBlocksComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LatestBlocksComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Block } from '../../interfaces/electrs.interface';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-latest-blocks',
|
||||
templateUrl: './latest-blocks.component.html',
|
||||
styleUrls: ['./latest-blocks.component.scss'],
|
||||
})
|
||||
export class LatestBlocksComponent implements OnInit, OnDestroy {
|
||||
blocks: any[] = [];
|
||||
blockSubscription: Subscription;
|
||||
isLoading = true;
|
||||
interval: any;
|
||||
trigger = 0;
|
||||
|
||||
constructor(
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.blockSubscription = this.stateService.blocks$
|
||||
.subscribe((block) => {
|
||||
if (block === null || !this.blocks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.height === this.blocks[0].height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are out of sync, reload the blocks instead
|
||||
if (block.height > this.blocks[0].height + 1) {
|
||||
this.loadInitialBlocks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.height === this.blocks[0].height) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.blocks.pop();
|
||||
this.blocks.unshift(block);
|
||||
});
|
||||
|
||||
this.loadInitialBlocks();
|
||||
this.interval = window.setInterval(() => this.trigger++, 1000 * 60);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.interval);
|
||||
this.blockSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
loadInitialBlocks() {
|
||||
this.electrsApiService.listBlocks$()
|
||||
.subscribe((blocks) => {
|
||||
this.blocks = blocks;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.isLoading = true;
|
||||
this.electrsApiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1)
|
||||
.subscribe((blocks) => {
|
||||
this.blocks = this.blocks.concat(blocks);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: Block) {
|
||||
return block.height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th>Transaction ID</th>
|
||||
<th style="width: 200px;">Value</th>
|
||||
<th style="width: 125px;">Size</th>
|
||||
<th style="width: 150px;">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>{{ transaction.value / 100000000 }} BTC</td>
|
||||
<td>{{ transaction.vsize | vbytes: 2 }}</td>
|
||||
<td>{{ transaction.fee / transaction.vsize | number : '1.2-2'}} sats/vB</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template [ngIf]="isLoading">
|
||||
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LatestTransactionsComponent } from './latest-transactions.component';
|
||||
|
||||
describe('LatestTransactionsComponent', () => {
|
||||
let component: LatestTransactionsComponent;
|
||||
let fixture: ComponentFixture<LatestTransactionsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LatestTransactionsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LatestTransactionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { Recent } from '../../interfaces/electrs.interface';
|
||||
import { flatMap, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-latest-transactions',
|
||||
templateUrl: './latest-transactions.component.html',
|
||||
styleUrls: ['./latest-transactions.component.scss']
|
||||
})
|
||||
export class LatestTransactionsComponent implements OnInit {
|
||||
transactions$: Observable<Recent[]>;
|
||||
isLoading = true;
|
||||
|
||||
constructor(
|
||||
private electrsApiService: ElectrsApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.transactions$ = timer(0, 10000)
|
||||
.pipe(
|
||||
flatMap(() => {
|
||||
return this.electrsApiService.getRecentTransaction$()
|
||||
.pipe(
|
||||
tap(() => this.isLoading = false)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" routerLink="/"><img src="./assets/mempool-space-logo.png" width="180" class="logo"> <span class="badge badge-warning" style="margin-left: 10px;" *ngIf="isOffline">Offline</span></a>
|
||||
|
||||
<button class="navbar-toggler" type="button" (click)="collapse()" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/tv" (click)="collapse()">TV view <img src="./assets/expand.png" width="15"/></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
|
||||
</li>
|
||||
</ul>
|
||||
<app-search-form location="top"></app-search-form>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<br />
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-master-page',
|
||||
templateUrl: './master-page.component.html',
|
||||
styleUrls: ['./master-page.component.scss']
|
||||
})
|
||||
export class MasterPageComponent implements OnInit {
|
||||
navCollapsed = false;
|
||||
isOffline = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.isOffline$
|
||||
.subscribe((state) => {
|
||||
this.isOffline = state;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="mempool-blocks-container">
|
||||
<div *ngFor="let projectedBlock of mempoolBlocks; let i = index; trackBy: trackByFn">
|
||||
<div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="getStyleForMempoolBlockAtIndex(i)">
|
||||
<div class="block-body" *ngIf="mempoolBlocks?.length">
|
||||
<div class="fees">
|
||||
~{{ projectedBlock.medianFee | ceil }} sat/vB
|
||||
<br/>
|
||||
<span class="yellow-color">{{ projectedBlock.feeRange[0] | ceil }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | ceil }} sat/vB</span>
|
||||
</div>
|
||||
<div class="block-size">{{ projectedBlock.blockSize | bytes: 2 }}</div>
|
||||
<div class="transaction-count">{{ projectedBlock.nTx }} transactions</div>
|
||||
<div class="time-difference" *ngIf="i !== 3">In ~{{ 10 * i + 10 }} minutes</div>
|
||||
<ng-template [ngIf]="i === 3 && mempoolBlocks?.length >= 4 && (projectedBlock.blockVSize / 1000000 | ceil) > 1">
|
||||
<div class="time-difference">+{{ projectedBlock.blockVSize / 1000000 | ceil }} blocks</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<span class="animated-border"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,6 @@
|
||||
.bitcoin-block {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.block-size {
|
||||
@@ -9,7 +8,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.projected-blocks-container {
|
||||
.mempool-blocks-container {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
@@ -20,7 +19,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.projected-block {
|
||||
.mempool-block {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
@@ -54,7 +53,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.projected-blocks-container {
|
||||
.mempool-blocks-container {
|
||||
position: absolute;
|
||||
left: -165px;
|
||||
top: -40px;
|
||||
@@ -87,11 +86,11 @@
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.projected-block.bitcoin-block::after {
|
||||
.mempool-block.bitcoin-block::after {
|
||||
background-color: #403834;
|
||||
}
|
||||
|
||||
.projected-block.bitcoin-block::before {
|
||||
.mempool-block.bitcoin-block::before {
|
||||
background-color: #2d2825;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component, OnInit, OnDestroy, Input, EventEmitter, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-blocks',
|
||||
templateUrl: './mempool-blocks.component.html',
|
||||
styleUrls: ['./mempool-blocks.component.scss']
|
||||
})
|
||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
mempoolBlocks: MempoolBlock[];
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
|
||||
blockWidth = 125;
|
||||
blockMarginLeft = 20;
|
||||
|
||||
@Input() txFeePerVSize: number;
|
||||
@Output() rightPosition: EventEmitter<number> = new EventEmitter<number>(true);
|
||||
@Output() blockDepth: EventEmitter<number> = new EventEmitter<number>(true);
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$
|
||||
.subscribe((blocks) => {
|
||||
this.mempoolBlocks = blocks;
|
||||
this.calculateTransactionPosition();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
getStyleForMempoolBlockAtIndex(index: number) {
|
||||
const greenBackgroundHeight = 100 - this.mempoolBlocks[index].blockVSize / 1000000 * 100;
|
||||
return {
|
||||
'right': 40 + index * 155 + 'px',
|
||||
'background': `repeating-linear-gradient(to right, #554b45, #554b45 ${greenBackgroundHeight}%,
|
||||
#bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
|
||||
};
|
||||
}
|
||||
|
||||
calculateTransactionPosition() {
|
||||
if (!this.txFeePerVSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const block of this.mempoolBlocks) {
|
||||
for (let i = 0; i < block.feeRange.length - 1; i++) {
|
||||
if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
|
||||
const txInBlockIndex = this.mempoolBlocks.indexOf(block);
|
||||
const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]);
|
||||
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
|
||||
|
||||
const txFee = this.txFeePerVSize - block.feeRange[i];
|
||||
const max = block.feeRange[i + 1] - block.feeRange[i];
|
||||
const blockLocation = txFee / max;
|
||||
|
||||
const chunkPositionOffset = blockLocation * feeRangeChunkSize;
|
||||
const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
|
||||
|
||||
const blockedFilledPercentage = (block.blockVSize > 1000000 ? 1000000 : block.blockVSize) / 1000000;
|
||||
|
||||
const arrowRightPosition = txInBlockIndex * (this.blockMarginLeft + this.blockWidth)
|
||||
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
||||
|
||||
this.rightPosition.next(arrowRightPosition);
|
||||
this.blockDepth.next(txInBlockIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
frontend/src/app/components/qrcode/qrcode.component.html
Normal file
1
frontend/src/app/components/qrcode/qrcode.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<canvas #canvas></canvas>
|
||||
25
frontend/src/app/components/qrcode/qrcode.component.spec.ts
Normal file
25
frontend/src/app/components/qrcode/qrcode.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { QrcodeComponent } from './qrcode.component';
|
||||
|
||||
describe('QrcodeComponent', () => {
|
||||
let component: QrcodeComponent;
|
||||
let fixture: ComponentFixture<QrcodeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ QrcodeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(QrcodeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
43
frontend/src/app/components/qrcode/qrcode.component.ts
Normal file
43
frontend/src/app/components/qrcode/qrcode.component.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Component, Input, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
import * as QRCode from 'qrcode/build/qrcode.js';
|
||||
|
||||
@Component({
|
||||
selector: 'app-qrcode',
|
||||
templateUrl: './qrcode.component.html',
|
||||
styleUrls: ['./qrcode.component.scss']
|
||||
})
|
||||
export class QrcodeComponent implements AfterViewInit, OnDestroy {
|
||||
@Input() data: string;
|
||||
@ViewChild('canvas') canvas: ElementRef;
|
||||
|
||||
qrcodeObject: any;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
const opts = {
|
||||
errorCorrectionLevel: 'H',
|
||||
margin: 0,
|
||||
color: {
|
||||
dark: '#000',
|
||||
light: '#fff'
|
||||
},
|
||||
width: 125,
|
||||
height: 125,
|
||||
};
|
||||
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRCode.toCanvas(this.canvas.nativeElement, this.data.toUpperCase(), opts, (error: any) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
</ng-template>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SearchFormComponent } from './search-form.component';
|
||||
|
||||
describe('SearchFormComponent', () => {
|
||||
let component: SearchFormComponent;
|
||||
let fixture: ComponentFixture<SearchFormComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ SearchFormComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-form',
|
||||
templateUrl: './search-form.component.html',
|
||||
styleUrls: ['./search-form.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() location: string;
|
||||
searchForm: FormGroup;
|
||||
|
||||
searchButtonText = 'Search';
|
||||
searchBoxPlaceholderText = 'Transaction, address, block hash...';
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
search() {
|
||||
const searchText = this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
if (this.regexAddress.test(searchText)) {
|
||||
this.router.navigate(['/address/', searchText]);
|
||||
} else {
|
||||
this.router.navigate(['/tx/', searchText]);
|
||||
}
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
20
frontend/src/app/components/start/start.component.html
Normal file
20
frontend/src/app/components/start/start.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<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>
|
||||
3
frontend/src/app/components/start/start.component.scss
Normal file
3
frontend/src/app/components/start/start.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.search-container {
|
||||
padding-top: 50px;
|
||||
}
|
||||
25
frontend/src/app/components/start/start.component.spec.ts
Normal file
25
frontend/src/app/components/start/start.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StartComponent } from './start.component';
|
||||
|
||||
describe('StartComponent', () => {
|
||||
let component: StartComponent;
|
||||
let fixture: ComponentFixture<StartComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StartComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StartComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
frontend/src/app/components/start/start.component.ts
Normal file
10
frontend/src/app/components/start/start.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-start',
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss']
|
||||
})
|
||||
export class StartComponent {
|
||||
view: 'blocks' | 'transactions' = 'blocks';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../styles.scss";
|
||||
@import "../../../styles.scss";
|
||||
|
||||
.ct-bar-label {
|
||||
font-size: 20px;
|
||||
@@ -49,10 +49,7 @@
|
||||
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/graphs']" fragment="6m"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1y'" disabled> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'" disabled> All
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { formatDate } from '@angular/common';
|
||||
import { VbytesPipe } from '../shared/pipes/bytes-pipe/vbytes.pipe';
|
||||
|
||||
import * as Chartist from 'chartist';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { IMempoolStats } from '../blockchain/interfaces';
|
||||
import { of, merge} from 'rxjs';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MemPoolService } from '../services/mem-pool.service';
|
||||
|
||||
import { VbytesPipe } from '../../pipes/bytes-pipe/vbytes.pipe';
|
||||
import { MempoolStats } from '../../interfaces/node-api.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
import * as Chartist from 'chartist';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-statistics',
|
||||
@@ -20,7 +22,7 @@ export class StatisticsComponent implements OnInit {
|
||||
loading = true;
|
||||
spinnerLoading = false;
|
||||
|
||||
mempoolStats: IMempoolStats[] = [];
|
||||
mempoolStats: MempoolStats[] = [];
|
||||
|
||||
mempoolVsizeFeesData: any;
|
||||
mempoolUnconfirmedTransactionsData: any;
|
||||
@@ -32,15 +34,16 @@ export class StatisticsComponent implements OnInit {
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private vbytesPipe: VbytesPipe,
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private memPoolService: MemPoolService,
|
||||
private websocketService: WebsocketService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
'dateSpan': '2h'
|
||||
dateSpan: '2h'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ export class StatisticsComponent implements OnInit {
|
||||
const labelInterpolationFnc = (value: any, index: any) => {
|
||||
const nr = 6;
|
||||
|
||||
switch (this.radioGroupForm.controls['dateSpan'].value) {
|
||||
switch (this.radioGroupForm.controls.dateSpan.value) {
|
||||
case '2h':
|
||||
case '24h':
|
||||
value = formatDate(value, 'HH:mm', this.locale);
|
||||
@@ -122,13 +125,13 @@ export class StatisticsComponent implements OnInit {
|
||||
.fragment
|
||||
.subscribe((fragment) => {
|
||||
if (['2h', '24h', '1w', '1m', '3m', '6m'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls['dateSpan'].setValue(fragment, { emitEvent: false });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
|
||||
merge(
|
||||
of(''),
|
||||
this.radioGroupForm.controls['dateSpan'].valueChanges
|
||||
this.radioGroupForm.controls.dateSpan.valueChanges
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.mempoolStats = [];
|
||||
@@ -138,34 +141,34 @@ export class StatisticsComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
this.spinnerLoading = true;
|
||||
if (this.radioGroupForm.controls['dateSpan'].value === '2h') {
|
||||
this.apiService.webSocketWant(['live-2h-chart']);
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
|
||||
this.websocketService.want(['live-2h-chart']);
|
||||
return this.apiService.list2HStatistics$();
|
||||
}
|
||||
this.apiService.webSocketWant([]);
|
||||
if (this.radioGroupForm.controls['dateSpan'].value === '24h') {
|
||||
this.websocketService.want([]);
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '24h') {
|
||||
return this.apiService.list24HStatistics$();
|
||||
}
|
||||
if (this.radioGroupForm.controls['dateSpan'].value === '1w') {
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '1w') {
|
||||
return this.apiService.list1WStatistics$();
|
||||
}
|
||||
if (this.radioGroupForm.controls['dateSpan'].value === '1m') {
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '1m') {
|
||||
return this.apiService.list1MStatistics$();
|
||||
}
|
||||
if (this.radioGroupForm.controls['dateSpan'].value === '3m') {
|
||||
if (this.radioGroupForm.controls.dateSpan.value === '3m') {
|
||||
return this.apiService.list3MStatistics$();
|
||||
}
|
||||
return this.apiService.list6MStatistics$();
|
||||
})
|
||||
)
|
||||
.subscribe((mempoolStats) => {
|
||||
.subscribe((mempoolStats: any) => {
|
||||
this.mempoolStats = mempoolStats;
|
||||
this.handleNewMempoolData(this.mempoolStats.concat([]));
|
||||
this.loading = false;
|
||||
this.spinnerLoading = false;
|
||||
});
|
||||
|
||||
this.memPoolService.live2Chart$
|
||||
this.stateService.live2Chart$
|
||||
.subscribe((mempoolStats) => {
|
||||
this.mempoolStats.unshift(mempoolStats);
|
||||
this.mempoolStats = this.mempoolStats.slice(0, this.mempoolStats.length - 1);
|
||||
@@ -173,7 +176,7 @@ export class StatisticsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: IMempoolStats[]) {
|
||||
handleNewMempoolData(mempoolStats: MempoolStats[]) {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
|
||||
@@ -193,7 +196,7 @@ export class StatisticsComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
generateArray(mempoolStats: IMempoolStats[]) {
|
||||
generateArray(mempoolStats: MempoolStats[]) {
|
||||
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="text-center" class="blockchain-wrapper">
|
||||
<div class="position-container">
|
||||
<app-blockchain-projected-blocks></app-blockchain-projected-blocks>
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
<app-blockchain-blocks></app-blockchain-blocks>
|
||||
|
||||
<div id="divider"></div>
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Component, OnInit, LOCALE_ID, Inject, Renderer2 } from '@angular/core';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { formatDate } from '@angular/common';
|
||||
import { VbytesPipe } from '../shared/pipes/bytes-pipe/vbytes.pipe';
|
||||
import { VbytesPipe } from '../../pipes/bytes-pipe/vbytes.pipe';
|
||||
|
||||
import * as Chartist from 'chartist';
|
||||
import { IMempoolStats } from '../blockchain/interfaces';
|
||||
import { MemPoolService } from '../services/mem-pool.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { MempoolStats } from '../../interfaces/node-api.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-television',
|
||||
@@ -15,20 +16,21 @@ import { MemPoolService } from '../services/mem-pool.service';
|
||||
export class TelevisionComponent implements OnInit {
|
||||
loading = true;
|
||||
|
||||
mempoolStats: IMempoolStats[] = [];
|
||||
mempoolStats: MempoolStats[] = [];
|
||||
mempoolVsizeFeesData: any;
|
||||
mempoolVsizeFeesOptions: any;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private vbytesPipe: VbytesPipe,
|
||||
private memPoolService: MemPoolService,
|
||||
private renderer: Renderer2,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.apiService.webSocketWant(['projected-blocks', 'live-2h-chart']);
|
||||
this.websocketService.want(['live-2h-chart']);
|
||||
|
||||
this.renderer.addClass(document.body, 'disable-scroll');
|
||||
|
||||
@@ -78,7 +80,7 @@ export class TelevisionComponent implements OnInit {
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.memPoolService.live2Chart$
|
||||
this.stateService.live2Chart$
|
||||
.subscribe((mempoolStats) => {
|
||||
this.mempoolStats.unshift(mempoolStats);
|
||||
this.mempoolStats = this.mempoolStats.slice(0, this.mempoolStats.length - 1);
|
||||
@@ -86,7 +88,7 @@ export class TelevisionComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: IMempoolStats[]) {
|
||||
handleNewMempoolData(mempoolStats: MempoolStats[]) {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
|
||||
@@ -101,7 +103,7 @@ export class TelevisionComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
generateArray(mempoolStats: IMempoolStats[]) {
|
||||
generateArray(mempoolStats: MempoolStats[]) {
|
||||
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-time-since',
|
||||
template: `{{ time | timeSince : trigger }}`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TimeSinceComponent implements OnInit, OnDestroy {
|
||||
interval: number;
|
||||
trigger = 0;
|
||||
|
||||
@Input() time: number;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.interval = window.setInterval(() => {
|
||||
this.trigger++;
|
||||
this.ref.markForCheck();
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<div class="container">
|
||||
|
||||
<app-blockchain></app-blockchain>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<h1>Transaction</h1>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||
|
||||
<ng-template [ngIf]="tx.status.confirmed" [ngIfElse]="unconfirmedTemplate">
|
||||
|
||||
<div class="box">
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td class="adjust-btn-padding">
|
||||
<ng-template [ngIf]="latestBlock">
|
||||
<button type="button" class="btn btn-sm btn-success">{{ latestBlock.height - tx.status.block_height + 1 }} confirmation<ng-container *ngIf="latestBlock.height - tx.status.block_height + 1 > 1">s</ng-container></button>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fees per vByte</td>
|
||||
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #unconfirmedTemplate>
|
||||
|
||||
<div class="box">
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Transaction</td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx/', txId]">{{ txId | shortenString }}</a>
|
||||
<app-clipboard [text]="txId"></app-clipboard>
|
||||
</td>
|
||||
<td class="adjust-btn-padding">
|
||||
<button type="button" class="btn btn-sm btn-danger">Unconfirmed</button>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
<td>{{ tx.fee / (tx.weight / 4) | number : '1.2-2' }} sat/vB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
<h2>Inputs & Outputs</h2>
|
||||
|
||||
<app-transactions-list [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>
|
||||
|
||||
<h2>Details</h2>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ tx.size | bytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weight</td>
|
||||
<td>{{ tx.weight | wuBytes: 2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingTx && !error">
|
||||
|
||||
<div class="box">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="spinner-border"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading transaction data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -0,0 +1,12 @@
|
||||
.adjust-btn-padding {
|
||||
padding: 0.55rem;
|
||||
}
|
||||
|
||||
#arrow {
|
||||
position: absolute;
|
||||
bottom: -24px;
|
||||
width: 40px;
|
||||
right: -1px;
|
||||
|
||||
width: 40px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Transaction, Block } from '../../interfaces/electrs.interface';
|
||||
import { of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
templateUrl: './transaction.component.html',
|
||||
styleUrls: ['./transaction.component.scss']
|
||||
})
|
||||
export class TransactionComponent implements OnInit {
|
||||
tx: Transaction;
|
||||
txId: string;
|
||||
isLoadingTx = true;
|
||||
conversions: any;
|
||||
error: any = undefined;
|
||||
latestBlock: Block;
|
||||
|
||||
rightPosition = 0;
|
||||
blockDepth = 0;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.txId = params.get('id') || '';
|
||||
this.error = undefined;
|
||||
this.isLoadingTx = true;
|
||||
if (history.state.data) {
|
||||
return of(history.state.data);
|
||||
} else {
|
||||
return this.electrsApiService.getTransaction$(this.txId);
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe((tx: Transaction) => {
|
||||
this.tx = tx;
|
||||
this.isLoadingTx = false;
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
if (!tx.status.confirmed) {
|
||||
this.websocketService.startTrackTx(tx.txid);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
this.isLoadingTx = false;
|
||||
});
|
||||
|
||||
this.stateService.conversions$
|
||||
.subscribe((conversions) => this.conversions = conversions);
|
||||
|
||||
this.stateService.blocks$
|
||||
.subscribe((block) => this.latestBlock = block);
|
||||
|
||||
this.stateService.txConfirmed
|
||||
.subscribe((block) => {
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<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>
|
||||
<div class="header-bg box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr *ngFor="let vin of tx.vin">
|
||||
<td class="arrow-td">
|
||||
<a [routerLink]="['/tx/', vin.txid]">
|
||||
<i class="arrow green"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
|
||||
Coinbase
|
||||
</ng-template>
|
||||
<ng-template #regularVin>
|
||||
<a [routerLink]="['/address/', vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">{{ vin.prevout.scriptpubkey_address | shortenString : 42 }}</a>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right nowrap">
|
||||
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
|
||||
<tbody>
|
||||
<tr *ngFor="let vout of tx.vout; let vindex = index;">
|
||||
<td>
|
||||
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/', vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">{{ vout.scriptpubkey_address | shortenString : 42 }}</a>
|
||||
<ng-template #scriptpubkey_type>
|
||||
OP_RETURN
|
||||
</ng-template>
|
||||
<!--
|
||||
<div>
|
||||
<app-address-labels [vout]="vout"></app-address-labels>
|
||||
</div>
|
||||
-->
|
||||
</td>
|
||||
<td class="text-right nowrap">
|
||||
<app-amount [satoshis]="vout.value"></app-amount>
|
||||
</td>
|
||||
<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>
|
||||
<ng-template #spent>
|
||||
<a [routerLink]="['/tx/', outspends[i][vindex].txid]"><i class="arrow red"></i></a>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="4">
|
||||
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||
<button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-sm btn-success mt-3">{{ latestBlock.height - tx.status.block_height + 1 }} confirmations</button>
|
||||
<ng-template #unconfirmedButton>
|
||||
<button type="button" class="btn btn-sm btn-danger mt-3">Unconfirmed</button>
|
||||
</ng-template>
|
||||
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-primary mt-3" (click)="switchCurrency()">
|
||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</ng-container>
|
||||
@@ -0,0 +1,64 @@
|
||||
.header-bg {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.arrow-td {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block!important;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 22px;
|
||||
box-sizing: content-box
|
||||
}
|
||||
|
||||
.arrow:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: calc(-1*30px/3);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 6.66px solid transparent;
|
||||
border-bottom: 6.66px solid transparent
|
||||
}
|
||||
|
||||
.arrow:after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: calc(30px/6);
|
||||
width: calc(30px/3);
|
||||
height: calc(20px/3);
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.arrow.green:before {
|
||||
border-left: 10px solid #28a745;
|
||||
}
|
||||
.arrow.green:after {
|
||||
background-color:#28a745;
|
||||
}
|
||||
|
||||
.arrow.red:before {
|
||||
border-left: 10px solid #dc3545;
|
||||
}
|
||||
.arrow.red:after {
|
||||
background-color:#dc3545;
|
||||
}
|
||||
|
||||
.arrow.grey:before {
|
||||
border-left: 10px solid #6c757d;
|
||||
}
|
||||
|
||||
.arrow.grey:after {
|
||||
background-color:#6c757d;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { Block, Outspend } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions-list',
|
||||
templateUrl: './transactions-list.component.html',
|
||||
styleUrls: ['./transactions-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() transactions: any[];
|
||||
@Input() showConfirmations = false;
|
||||
@Input() transactionPage = false;
|
||||
|
||||
latestBlock$: Observable<Block>;
|
||||
outspends: Outspend[] = [];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private ref: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.latestBlock$ = this.stateService.blocks$;
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (!this.transactions || !this.transactions.length) {
|
||||
return;
|
||||
}
|
||||
const observableObject = {};
|
||||
this.transactions.forEach((tx, i) => {
|
||||
if (this.outspends[i]) {
|
||||
return;
|
||||
}
|
||||
observableObject[i] = this.electrsApiService.getOutspends$(tx.txid);
|
||||
});
|
||||
|
||||
forkJoin(observableObject)
|
||||
.subscribe((outspends: any) => {
|
||||
const newOutspends = [];
|
||||
for (const i in outspends) {
|
||||
if (outspends.hasOwnProperty(i)) {
|
||||
newOutspends.push(outspends[i]);
|
||||
}
|
||||
}
|
||||
this.outspends = this.outspends.concat(newOutspends);
|
||||
this.ref.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
getTotalTxOutput(tx: any) {
|
||||
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||
}
|
||||
|
||||
switchCurrency() {
|
||||
const oldvalue = !this.stateService.viewFiat$.value;
|
||||
this.stateService.viewFiat$.next(oldvalue);
|
||||
}
|
||||
|
||||
trackByFn(index: number) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<div class="container">
|
||||
<h1>Address</h1>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAddress && !error">
|
||||
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr class="header-bg">
|
||||
<th>
|
||||
<a [routerLink]="['/explorer/address/', address.address]">{{ address.address }}</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Number of transactions</td>
|
||||
<td>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total received</td>
|
||||
<td>{{ (address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total sent</td>
|
||||
<td>{{ (address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<div class="qr-wrapper">
|
||||
<qrcode id="qrCode" [qrdata]="address.address" [size]="128" [level]="'M'"></qrcode>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>{{ transactions?.length || '?' }} of {{ address.chain_stats.tx_count + address.mempool_stats.tx_count }} transactions</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="spinner-border text-light"></div>
|
||||
<br><br>
|
||||
</ng-template>
|
||||
<button *ngIf="transactions?.length && transactions?.length !== (address.chain_stats.tx_count + address.mempool_stats.tx_count)" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAddress && !error">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading address data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,86 +0,0 @@
|
||||
<div class="container">
|
||||
<h1>Block <ng-template [ngIf]="!isLoadingBlock"><a [routerLink]="['/explorer/block/', block.id]">#{{ block.height }}</a></ng-template></h1>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Timestamp</td>
|
||||
<td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} ({{ block.timestamp | timeSince }} ago)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number of transactions</td>
|
||||
<td>{{ block.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ block.size | bytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weight</td>
|
||||
<td>{{ block.weight | wuBytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>{{ (latestBlockHeight - block.height + 1) }} confirmation{{ (latestBlockHeight - block.height + 1) === 1 ? '' : 's' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hash</td>
|
||||
<td><a [routerLink]="['/explorer/block/', block.id]">{{ block.id | shortenString : 36 }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Previous Block</td>
|
||||
<td><a [routerLink]="['/explorer/block/', block.previousblockhash]">{{ block.previousblockhash | shortenString : 36 }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>{{ transactions?.length || '?' }} of {{ block.tx_count }} transactions</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<app-transactions-list [transactions]="transactions"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="spinner-border text-light"></div>
|
||||
<br><br>
|
||||
</ng-template>
|
||||
<button *ngIf="transactions?.length && transactions?.length !== block.tx_count" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingBlock && !error">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading block data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,40 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { QRCodeModule } from 'angularx-qrcode';
|
||||
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';
|
||||
import { AddressComponent } from './address/address.component';
|
||||
import { TransactionsListComponent } from './transactions-list/transactions-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ExplorerComponent,
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: TransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
component: AddressComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [ExplorerComponent, TransactionComponent, BlockComponent, AddressComponent, TransactionsListComponent],
|
||||
imports: [
|
||||
SharedModule,
|
||||
CommonModule,
|
||||
RouterModule.forChild(routes),
|
||||
QRCodeModule,
|
||||
]
|
||||
})
|
||||
export class ExplorerModule { }
|
||||
@@ -1,34 +0,0 @@
|
||||
<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</th>
|
||||
<th>Weight</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 | wuBytes: 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>
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<div class="container">
|
||||
<h1>Transaction</h1>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||
|
||||
<app-transactions-list [transactions]="[tx]" [showConfirmations]="true"></app-transactions-list>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>Details</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ tx.size | bytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weight</td>
|
||||
<td>{{ tx.weight | wuBytes: 2 }}</td>
|
||||
</tr>
|
||||
<tr *ngIf="tx.status.confirmed">
|
||||
<td>Included in block</td>
|
||||
<td><a [routerLink]="['/explorer/block/', tx.status.block_hash]">#{{ tx.status.block_height }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped" *ngIf="tx.fee">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Fees</td>
|
||||
<td>{{ tx.fee }} <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee | currency:'USD':'symbol':'1.2-2' }}</span>)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fees per vByte</td>
|
||||
<td>{{ (tx.fee * 100000000) / tx.vsize | number : '1.2-2' }} sat/vB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf="isLoadingTx && !error">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
<br><br>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading transaction data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { MemPoolService } from 'src/app/services/mem-pool.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
templateUrl: './transaction.component.html',
|
||||
styleUrls: ['./transaction.component.scss']
|
||||
})
|
||||
export class TransactionComponent implements OnInit {
|
||||
tx: any;
|
||||
isLoadingTx = true;
|
||||
conversions: any;
|
||||
error: any;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private memPoolService: MemPoolService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
const txId: string = params.get('id') || '';
|
||||
return this.apiService.getTransaction$(txId);
|
||||
})
|
||||
)
|
||||
.subscribe((tx) => {
|
||||
this.tx = tx;
|
||||
this.isLoadingTx = false;
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
this.isLoadingTx = false;
|
||||
});
|
||||
|
||||
this.memPoolService.conversions$
|
||||
.subscribe((conversions) => {
|
||||
this.conversions = conversions;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<ng-template ngFor let-tx [ngForOf]="transactions">
|
||||
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<tr class="header-bg">
|
||||
<th>
|
||||
<a [routerLink]="['/explorer/tx/', tx.txid]">{{ tx.txid }}</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div *ngFor="let vin of tx.vin">
|
||||
<ng-template [ngIf]="vin.is_coinbase" [ngIfElse]="regularVin">
|
||||
Coinbase
|
||||
</ng-template>
|
||||
<ng-template #regularVin>
|
||||
<a [routerLink]="['/explorer/address/', vin.prevout.scriptpubkey_address]">{{ vin.prevout.scriptpubkey_address }}</a>
|
||||
(<a [routerLink]="['/explorer/tx/', vin.txid]">tx</a>)
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div *ngFor="let vin of tx.vin">
|
||||
<ng-template [ngIf]="vin.prevout">
|
||||
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVin">
|
||||
<span class="green-color">{{ conversions.USD * (vin.prevout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-template>
|
||||
<ng-template #viewFiatVin>
|
||||
{{ vin.prevout.value / 100000000 }} BTC
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div *ngFor="let vout of tx.vout">
|
||||
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/explorer/address/', vout.scriptpubkey_address]">{{ vout.scriptpubkey_address }}</a>
|
||||
<ng-template #scriptpubkey_type>
|
||||
{{ vout.scriptpubkey_type | uppercase }}
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div *ngFor="let vout of tx.vout">
|
||||
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatVout">
|
||||
<span class="green-color">{{ conversions.USD * (vout.value / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-template>
|
||||
<ng-template #viewFiatVout>
|
||||
{{ vout.value / 100000000 }} BTC
|
||||
</ng-template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="4">
|
||||
<ng-template [ngIf]="showConfirmations">
|
||||
<button *ngIf="tx.status.confirmed; else unconfirmedButton" type="button" class="btn btn-success">{{ latestBlockHeight - tx.status.block_height + 1 }} confirmations</button>
|
||||
<ng-template #unconfirmedButton>
|
||||
<button type="button" class="btn btn-danger">Unconfirmed</button>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
<button type="button" class="btn btn-primary" (click)="viewFiat = !viewFiat">
|
||||
<ng-template [ngIf]="viewFiat" [ngIfElse]="viewFiatButton">
|
||||
<span *ngIf="conversions">{{ conversions.USD * (getTotalTxOutput(tx) / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
||||
</ng-template>
|
||||
<ng-template #viewFiatButton>
|
||||
{{ getTotalTxOutput(tx) / 100000000 }} BTC
|
||||
</ng-template>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.header-bg {
|
||||
background-color:#653b9c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-bg a {
|
||||
color: #FFF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { MemPoolService } from 'src/app/services/mem-pool.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions-list',
|
||||
templateUrl: './transactions-list.component.html',
|
||||
styleUrls: ['./transactions-list.component.scss']
|
||||
})
|
||||
export class TransactionsListComponent implements OnInit {
|
||||
@Input() transactions: any[];
|
||||
@Input() showConfirmations = false;
|
||||
latestBlockHeight: number;
|
||||
|
||||
viewFiat = false;
|
||||
conversions: any;
|
||||
|
||||
constructor(
|
||||
private memPoolService: MemPoolService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.memPoolService.conversions$
|
||||
.subscribe((conversions) => {
|
||||
this.conversions = conversions;
|
||||
});
|
||||
|
||||
this.memPoolService.blocks$
|
||||
.subscribe((block) => {
|
||||
this.latestBlockHeight = block.height;
|
||||
});
|
||||
}
|
||||
|
||||
getTotalTxOutput(tx: any) {
|
||||
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<div style="height: 400px;" *ngIf="mempoolVsizeFeesData; else loadingFees">
|
||||
<form [formGroup]="radioGroupForm" style="position: absolute;">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="model">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" value="line"> Line
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" value="pie"> Pie
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<app-chartist
|
||||
*ngIf="radioGroupForm.get('model')?.value === 'pie'"
|
||||
[data]="mempoolVsizeFeesPieData"
|
||||
[type]="'Pie'"
|
||||
[options]="mempoolVsizeFeesPieOptions">
|
||||
</app-chartist>
|
||||
<app-chartist
|
||||
*ngIf="radioGroupForm.get('model')?.value === 'line'"
|
||||
[data]="mempoolVsizeFeesData"
|
||||
[type]="'Bar'"
|
||||
[options]="mempoolVsizeFeesOptions">
|
||||
</app-chartist>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingFees>
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import * as Chartist from 'chartist';
|
||||
import { ApiService } from '../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fee-distribution-graph',
|
||||
templateUrl: './fee-distribution-graph.component.html',
|
||||
styleUrls: ['./fee-distribution-graph.component.scss']
|
||||
})
|
||||
export class FeeDistributionGraphComponent implements OnInit {
|
||||
@Input() projectedBlockIndex: number;
|
||||
@Input() blockHeight: number;
|
||||
|
||||
mempoolVsizeFeesData: any;
|
||||
mempoolVsizeFeesOptions: any;
|
||||
|
||||
mempoolVsizeFeesPieData: any;
|
||||
mempoolVsizeFeesPieOptions: any;
|
||||
|
||||
feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500];
|
||||
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private apiService: ApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
model: ['line'],
|
||||
});
|
||||
|
||||
this.mempoolVsizeFeesOptions = {
|
||||
showArea: false,
|
||||
showLine: false,
|
||||
fullWidth: false,
|
||||
showPoint: false,
|
||||
low: 0,
|
||||
axisX: {
|
||||
position: 'start',
|
||||
showLabel: false,
|
||||
offset: 0,
|
||||
showGrid: false,
|
||||
},
|
||||
axisY: {
|
||||
position: 'end',
|
||||
scaleMinSpace: 40,
|
||||
showGrid: false,
|
||||
},
|
||||
plugins: [
|
||||
Chartist.plugins.tooltip({
|
||||
tooltipOffset: {
|
||||
x: 15,
|
||||
y: 250
|
||||
},
|
||||
transformTooltipTextFnc: (value: number): any => {
|
||||
return Math.ceil(value) + ' sat/vB';
|
||||
},
|
||||
anchorToPoint: false,
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
this.mempoolVsizeFeesPieOptions = {
|
||||
showLabel: false,
|
||||
plugins: [
|
||||
Chartist.plugins.tooltip({
|
||||
tooltipOffset: {
|
||||
x: 15,
|
||||
y: 250
|
||||
},
|
||||
transformTooltipTextFnc: (value: string, seriesName: string): any => {
|
||||
const index = parseInt(seriesName.split(' ')[2].split('-')[1], 10);
|
||||
const intValue = parseInt(value, 10);
|
||||
const result = Math.ceil(intValue) + ' tx @ ' + this.feeLevels[index] +
|
||||
(this.feeLevels[index + 1] ? '-' + this.feeLevels[index + 1] : '+' ) + ' sat/vB';
|
||||
|
||||
return result;
|
||||
},
|
||||
anchorToPoint: false,
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
let sub;
|
||||
if (this.blockHeight) {
|
||||
sub = this.apiService.listTransactionsForBlock$(this.blockHeight);
|
||||
} else {
|
||||
sub = this.apiService.listTransactionsForProjectedBlock$(this.projectedBlockIndex);
|
||||
}
|
||||
|
||||
sub.subscribe((data) => {
|
||||
const fees = data.map((tx) => tx.fpv);
|
||||
|
||||
const series = [];
|
||||
|
||||
for (let i = 0; i < this.feeLevels.length; i++) {
|
||||
let total = 0;
|
||||
for (let j = 0; j < fees.length; j++) {
|
||||
if (i === this.feeLevels.length - 1) {
|
||||
if (fees[j] >= this.feeLevels[i]) {
|
||||
total += 1;
|
||||
}
|
||||
} else if (fees[j] >= this.feeLevels[i] && fees[j] < this.feeLevels[i + 1]) {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
series.push(total);
|
||||
}
|
||||
|
||||
this.mempoolVsizeFeesPieData = {
|
||||
series: series.map((d, index: number) => {
|
||||
return {
|
||||
value: d,
|
||||
className: 'ct-series-' + Chartist.alphaNumerate(index) + ' index-' + index
|
||||
};
|
||||
}),
|
||||
labels: data.map((x, i) => i),
|
||||
};
|
||||
|
||||
this.mempoolVsizeFeesData = {
|
||||
labels: data.map((x, i) => i),
|
||||
series: [fees]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<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>
|
||||
@@ -1,44 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MemPoolService, IMemPoolState } from '../services/mem-pool.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
memPoolInfo: IMemPoolState | undefined;
|
||||
mempoolBlocks = 0;
|
||||
progressWidth = '';
|
||||
progressClass: string;
|
||||
mempoolSize = 0;
|
||||
|
||||
constructor(
|
||||
private memPoolService: MemPoolService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.memPoolService.mempoolStats$
|
||||
.subscribe((mempoolState) => {
|
||||
this.memPoolInfo = mempoolState;
|
||||
this.updateProgress();
|
||||
});
|
||||
|
||||
this.memPoolService.projectedBlocks$
|
||||
.subscribe((projectedblocks) => {
|
||||
if (!projectedblocks.length) { return; }
|
||||
const size = projectedblocks.map((m) => m.blockSize).reduce((a, b) => a + b);
|
||||
const weight = projectedblocks.map((m) => m.blockWeight).reduce((a, b) => a + b);
|
||||
this.mempoolSize = size;
|
||||
this.mempoolBlocks = Math.ceil(weight / 4000000);
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user