Added first seen on mempool transactions.
This commit is contained in:
parent
23a61a37fd
commit
4879036216
@ -52,12 +52,25 @@ class Mempool {
|
|||||||
return this.vBytesPerSecond;
|
return this.vBytesPerSecond;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
||||||
|
const txTimes: number[] = [];
|
||||||
|
txIds.forEach((txId: string) => {
|
||||||
|
if (this.mempoolCache[txId]) {
|
||||||
|
txTimes.push(this.mempoolCache[txId].firstSeen);
|
||||||
|
} else {
|
||||||
|
txTimes.push(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return txTimes;
|
||||||
|
}
|
||||||
|
|
||||||
public async getTransactionExtended(txId: string): Promise<TransactionExtended | false> {
|
public async getTransactionExtended(txId: string): Promise<TransactionExtended | false> {
|
||||||
try {
|
try {
|
||||||
const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
|
const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
vsize: transaction.weight / 4,
|
vsize: transaction.weight / 4,
|
||||||
feePerVsize: transaction.fee / (transaction.weight / 4),
|
feePerVsize: transaction.fee / (transaction.weight / 4),
|
||||||
|
firstSeen: Math.round((new Date().getTime() / 1000)),
|
||||||
}, transaction);
|
}, transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(txId + ' not found');
|
console.log(txId + ' not found');
|
||||||
|
@ -71,6 +71,7 @@ class Server {
|
|||||||
|
|
||||||
setUpHttpApiRoutes() {
|
setUpHttpApiRoutes() {
|
||||||
this.app
|
this.app
|
||||||
|
.get(config.API_ENDPOINT + 'transaction-times', routes.getTransactionTimes)
|
||||||
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
|
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
|
||||||
.get(config.API_ENDPOINT + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
.get(config.API_ENDPOINT + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
||||||
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
|
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
|
||||||
|
@ -33,6 +33,7 @@ export interface TransactionExtended extends Transaction {
|
|||||||
size: number;
|
size: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
feePerVsize: number;
|
feePerVsize: number;
|
||||||
|
firstSeen: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Prevout {
|
export interface Prevout {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import statistics from './api/statistics';
|
import statistics from './api/statistics';
|
||||||
import feeApi from './api/fee-api';
|
import feeApi from './api/fee-api';
|
||||||
import mempoolBlocks from './api/mempool-blocks';
|
import mempoolBlocks from './api/mempool-blocks';
|
||||||
|
import mempool from './api/mempool';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
private cache = {};
|
private cache = {};
|
||||||
@ -62,6 +63,16 @@ class Routes {
|
|||||||
res.status(500).send(e.message);
|
res.status(500).send(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTransactionTimes(req, res) {
|
||||||
|
if (!Array.isArray(req.query.txId)) {
|
||||||
|
res.status(500).send('Not an array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const txIds = req.query.txId;
|
||||||
|
const times = mempool.getFirstSeenForTransactions(txIds);
|
||||||
|
res.send(times);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Routes();
|
export default new Routes();
|
||||||
|
@ -79,12 +79,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border"></div>
|
|
||||||
<br><br>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template [ngIf]="error">
|
<ng-template [ngIf]="error">
|
||||||
|
@ -6,6 +6,7 @@ import { Address, Transaction } from '../../interfaces/electrs.interface';
|
|||||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { AudioService } from 'src/app/services/audio.service';
|
import { AudioService } from 'src/app/services/audio.service';
|
||||||
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-address',
|
selector: 'app-address',
|
||||||
@ -17,9 +18,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
addressString: string;
|
addressString: string;
|
||||||
isLoadingAddress = true;
|
isLoadingAddress = true;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
|
tempTransactions: Transaction[];
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
error: any;
|
error: any;
|
||||||
|
|
||||||
|
|
||||||
txCount = 0;
|
txCount = 0;
|
||||||
receieved = 0;
|
receieved = 0;
|
||||||
sent = 0;
|
sent = 0;
|
||||||
@ -30,6 +33,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
|
private apiService: ApiService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -94,12 +98,31 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
loadAddress(addressStr?: string) {
|
loadAddress(addressStr?: string) {
|
||||||
this.electrsApiService.getAddress$(addressStr)
|
this.electrsApiService.getAddress$(addressStr)
|
||||||
.subscribe((address) => {
|
.pipe(
|
||||||
this.address = address;
|
switchMap((address) => {
|
||||||
this.updateChainStats();
|
this.address = address;
|
||||||
this.websocketService.startTrackAddress(address.address);
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.websocketService.startTrackAddress(address.address);
|
||||||
this.reloadAddressTransactions(address.address);
|
this.isLoadingAddress = false;
|
||||||
|
this.isLoadingTransactions = true;
|
||||||
|
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||||
|
}),
|
||||||
|
switchMap((transactions) => {
|
||||||
|
this.tempTransactions = transactions;
|
||||||
|
const fetchTxs = transactions.map((t) => t.txid);
|
||||||
|
return this.apiService.getTransactionTimes$(fetchTxs);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((times) => {
|
||||||
|
times.forEach((time, index) => {
|
||||||
|
this.tempTransactions[index].firstSeen = time;
|
||||||
|
});
|
||||||
|
this.tempTransactions.sort((a, b) => {
|
||||||
|
return b.status.block_time - a.status.block_time || b.firstSeen - a.firstSeen;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.transactions = this.tempTransactions;
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -114,16 +137,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
|
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
reloadAddressTransactions(address: string) {
|
|
||||||
this.isLoadingTransactions = true;
|
|
||||||
this.electrsApiService.getAddressTransactions$(address)
|
|
||||||
.subscribe((transactions: any) => {
|
|
||||||
this.transactions = transactions;
|
|
||||||
this.isLoadingTransactions = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.transactions[this.transactions.length - 1].txid)
|
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.transactions[this.transactions.length - 1].txid)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
<div class="block-size">{{ block.size | bytes: 2 }}</div>
|
||||||
<div class="transaction-count">{{ block.tx_count }} transactions</div>
|
<div class="transaction-count">{{ block.tx_count }} transactions</div>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<div class="time-difference">{{ block.timestamp | timeSince : trigger }} ago</div>
|
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,8 +14,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
blocks: Block[] = [];
|
blocks: Block[] = [];
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
interval: any;
|
interval: any;
|
||||||
trigger = 0;
|
|
||||||
|
|
||||||
|
|
||||||
arrowVisible = false;
|
arrowVisible = false;
|
||||||
arrowLeftPx = 30;
|
arrowLeftPx = 30;
|
||||||
@ -35,8 +33,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.moveArrowToPosition();
|
this.moveArrowToPosition();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.interval = setInterval(() => this.trigger++, 10 * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||||
<td><a [routerLink]="['/block', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a></td>
|
<td><a [routerLink]="['/block', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a></td>
|
||||||
<td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
<td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||||
<td>{{ block.timestamp | timeSince : trigger }} ago</td>
|
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</td>
|
||||||
<td>{{ block.tx_count }}</td>
|
<td>{{ block.tx_count }}</td>
|
||||||
<td>{{ block.size | bytes: 2 }}</td>
|
<td>{{ block.size | bytes: 2 }}</td>
|
||||||
<td class="d-none d-md-block">
|
<td class="d-none d-md-block">
|
||||||
|
@ -14,7 +14,6 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
|
|||||||
blockSubscription: Subscription;
|
blockSubscription: Subscription;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
interval: any;
|
interval: any;
|
||||||
trigger = 0;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
@ -47,7 +46,6 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.loadInitialBlocks();
|
this.loadInitialBlocks();
|
||||||
this.interval = window.setInterval(() => this.trigger++, 1000 * 60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -10,6 +10,7 @@ export class TimeSinceComponent implements OnInit, OnDestroy {
|
|||||||
trigger = 0;
|
trigger = 0;
|
||||||
|
|
||||||
@Input() time: number;
|
@Input() time: number;
|
||||||
|
@Input() fastRender = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private ref: ChangeDetectorRef
|
private ref: ChangeDetectorRef
|
||||||
@ -19,7 +20,7 @@ export class TimeSinceComponent implements OnInit, OnDestroy {
|
|||||||
this.interval = window.setInterval(() => {
|
this.interval = window.setInterval(() => {
|
||||||
this.trigger++;
|
this.trigger++;
|
||||||
this.ref.markForCheck();
|
this.ref.markForCheck();
|
||||||
}, 1000 * 60);
|
}, 1000 * (this.fastRender ? 1 : 60));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -53,6 +53,18 @@
|
|||||||
|
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<ng-template [ngIf]="transactionTime !== 0">
|
||||||
|
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-template #firstSeenTmpl>
|
||||||
|
<tr>
|
||||||
|
<td>First seen</td>
|
||||||
|
<td><app-time-since [time]="transactionTime"></app-time-since> ago</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Fees</td>
|
<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 | number }} sats <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * tx.fee / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>)</span></td>
|
||||||
|
@ -7,6 +7,7 @@ import { of } from 'rxjs';
|
|||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { AudioService } from 'src/app/services/audio.service';
|
import { AudioService } from 'src/app/services/audio.service';
|
||||||
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transaction',
|
selector: 'app-transaction',
|
||||||
@ -20,6 +21,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
conversions: any;
|
conversions: any;
|
||||||
error: any = undefined;
|
error: any = undefined;
|
||||||
latestBlock: Block;
|
latestBlock: Block;
|
||||||
|
transactionTime = -1;
|
||||||
|
|
||||||
rightPosition = 0;
|
rightPosition = 0;
|
||||||
blockDepth = 0;
|
blockDepth = 0;
|
||||||
@ -30,6 +32,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
|
private apiService: ApiService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -55,6 +58,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
if (!tx.status.confirmed) {
|
if (!tx.status.confirmed) {
|
||||||
this.websocketService.startTrackTransaction(tx.txid);
|
this.websocketService.startTrackTransaction(tx.txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.getTransactionTime();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
@ -79,6 +84,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTransactionTime() {
|
||||||
|
this.apiService.getTransactionTimes$([this.tx.txid])
|
||||||
|
.subscribe((transactionTimes) => {
|
||||||
|
this.transactionTime = transactionTimes[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.websocketService.startTrackTransaction('stop');
|
this.websocketService.startTrackTransaction('stop');
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
||||||
<div *ngIf="!transactionPage" class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
<div *ngIf="!transactionPage" class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||||
<a [routerLink]="['/tx/', tx.txid]" [state]="{ data: tx }">{{ tx.txid }}</a>
|
<a [routerLink]="['/tx/', tx.txid]" [state]="{ data: tx }">{{ tx.txid }}</a>
|
||||||
<div class="float-right">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</div>
|
<div class="float-right">
|
||||||
|
<ng-template [ngIf]="tx.status.confirmed">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
|
||||||
|
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
|
||||||
|
<i><app-time-since [time]="tx.firstSeen"></app-time-since> ago</i>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-bg box">
|
<div class="header-bg box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -11,7 +11,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class TransactionsListComponent implements OnInit, OnChanges {
|
export class TransactionsListComponent implements OnInit, OnChanges {
|
||||||
@Input() transactions: any[];
|
@Input() transactions: Transaction[];
|
||||||
@Input() showConfirmations = false;
|
@Input() showConfirmations = false;
|
||||||
@Input() transactionPage = false;
|
@Input() transactionPage = false;
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ export interface Transaction {
|
|||||||
vin: Vin[];
|
vin: Vin[];
|
||||||
vout: Vout[];
|
vout: Vout[];
|
||||||
status: Status;
|
status: Status;
|
||||||
|
firstSeen?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Recent {
|
export interface Recent {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@ -40,4 +40,12 @@ export class ApiService {
|
|||||||
list1YStatistics$(): Observable<OptimizedMempoolStats[]> {
|
list1YStatistics$(): Observable<OptimizedMempoolStats[]> {
|
||||||
return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/1y');
|
return this.httpClient.get<OptimizedMempoolStats[]>(API_BASE_URL + '/statistics/1y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTransactionTimes$(txIds: string[]): Observable<number[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
txIds.forEach((txId: string) => {
|
||||||
|
params = params.append('txId[]', txId);
|
||||||
|
});
|
||||||
|
return this.httpClient.get<number[]>(API_BASE_URL + '/transaction-times', { params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user