New base code for mempool blockchain explorerer

This commit is contained in:
Simon Lindh
2020-02-16 22:15:07 +07:00
committed by wiz
parent d296239f54
commit 43f41b8aab
204 changed files with 6959 additions and 14341 deletions

View File

@@ -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]

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -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() { }
}

View File

@@ -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]
})

View File

@@ -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">&times;</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>

View File

@@ -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--;
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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%)`,
};
}
}
}

View File

@@ -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">&times;</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>

View File

@@ -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;
});
}
}

View File

@@ -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 };
}

View File

@@ -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>

View File

@@ -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([]);
}
}

View File

@@ -0,0 +1 @@
<span *ngIf="multisig" class="badge badge-pill badge-warning">multisig {{ multisigM }} of {{ multisigN }}</span>

View File

@@ -0,0 +1,3 @@
.badge {
margin-right: 2px;
}

View File

@@ -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();
});
});

View File

@@ -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;
}
}

View 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>

View File

@@ -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;

View File

@@ -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;

View 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>

View File

@@ -0,0 +1,3 @@
.green-color {
color: #3bcc49;
}

View 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();
});
});

View 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();
}
}

View File

@@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@@ -0,0 +1,7 @@
footer {
max-width: 960px;
}
.logo {
height: 40px;
}

View 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!');
});
});

View 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,
) { }
}

View 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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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%)`,
};
}

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View 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();
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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)
);
})
);
}
}

View File

@@ -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 &nbsp;<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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -0,0 +1 @@
<canvas #canvas></canvas>

View 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();
});
});

View 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() {
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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: '',
});
}
}
}

View 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>

View File

@@ -0,0 +1,3 @@
.search-container {
padding-top: 50px;
}

View 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();
});
});

View 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';
}

View File

@@ -1,4 +1,4 @@
@import "../../styles.scss";
@import "../../../styles.scss";
.ct-bar-label {
font-size: 20px;

View File

@@ -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>

View File

@@ -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];

View File

@@ -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>

View File

@@ -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];

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
.adjust-btn-padding {
padding: 0.55rem;
}
#arrow {
position: absolute;
bottom: -24px;
width: 40px;
right: -1px;
width: 40px;
}

View File

@@ -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,
};
});
}
}

View File

@@ -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>
&nbsp;
</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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 { }

View File

@@ -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>

View File

@@ -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;
});
}
}

View File

@@ -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>

View File

@@ -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;
});
}
}

View File

@@ -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>
&nbsp;
</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>

View File

@@ -1,9 +0,0 @@
.header-bg {
background-color:#653b9c;
font-size: 14px;
}
.header-bg a {
color: #FFF;
text-decoration: underline;
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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]
};
});
}
}

View File

@@ -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>&nbsp;<b>{{ memPoolInfo?.memPoolInfo?.size | number }}</b>
<br />
<span class="mempoolSize">Mempool size:</span>&nbsp;<b>{{ mempoolSize | bytes }} ({{ mempoolBlocks }} block<span [hidden]="mempoolBlocks <= 1">s</span>)</b>
<br />
<span class="txPerSecond">Tx weight per second:</span>&nbsp;
<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>

View File

@@ -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;
}

View File

@@ -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