Added first seen on mempool transactions.

This commit is contained in:
softsimon 2020-02-28 01:09:07 +07:00
parent 23a61a37fd
commit 4879036216
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
17 changed files with 100 additions and 34 deletions

View File

@ -52,12 +52,25 @@ class Mempool {
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> {
try {
const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
return Object.assign({
vsize: transaction.weight / 4,
feePerVsize: transaction.fee / (transaction.weight / 4),
firstSeen: Math.round((new Date().getTime() / 1000)),
}, transaction);
} catch (e) {
console.log(txId + ' not found');

View File

@ -71,6 +71,7 @@ class Server {
setUpHttpApiRoutes() {
this.app
.get(config.API_ENDPOINT + 'transaction-times', routes.getTransactionTimes)
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
.get(config.API_ENDPOINT + 'fees/mempool-blocks', routes.getMempoolBlocks)
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)

View File

@ -33,6 +33,7 @@ export interface TransactionExtended extends Transaction {
size: number;
vsize: number;
feePerVsize: number;
firstSeen: number;
}
export interface Prevout {

View File

@ -1,6 +1,7 @@
import statistics from './api/statistics';
import feeApi from './api/fee-api';
import mempoolBlocks from './api/mempool-blocks';
import mempool from './api/mempool';
class Routes {
private cache = {};
@ -62,6 +63,16 @@ class Routes {
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();

View File

@ -79,12 +79,6 @@
</div>
</div>
<br>
<div class="text-center">
<div class="spinner-border"></div>
<br><br>
</div>
</ng-template>
<ng-template [ngIf]="error">

View File

@ -6,6 +6,7 @@ import { Address, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from 'src/app/services/websocket.service';
import { StateService } from 'src/app/services/state.service';
import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-address',
@ -17,9 +18,11 @@ export class AddressComponent implements OnInit, OnDestroy {
addressString: string;
isLoadingAddress = true;
transactions: Transaction[];
tempTransactions: Transaction[];
isLoadingTransactions = true;
error: any;
txCount = 0;
receieved = 0;
sent = 0;
@ -30,6 +33,7 @@ export class AddressComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService,
private stateService: StateService,
private audioService: AudioService,
private apiService: ApiService,
) { }
ngOnInit() {
@ -94,12 +98,31 @@ export class AddressComponent implements OnInit, OnDestroy {
loadAddress(addressStr?: string) {
this.electrsApiService.getAddress$(addressStr)
.subscribe((address) => {
this.address = address;
this.updateChainStats();
this.websocketService.startTrackAddress(address.address);
this.isLoadingAddress = false;
this.reloadAddressTransactions(address.address);
.pipe(
switchMap((address) => {
this.address = address;
this.updateChainStats();
this.websocketService.startTrackAddress(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) => {
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;
}
reloadAddressTransactions(address: string) {
this.isLoadingTransactions = true;
this.electrsApiService.getAddressTransactions$(address)
.subscribe((transactions: any) => {
this.transactions = transactions;
this.isLoadingTransactions = false;
});
}
loadMore() {
this.isLoadingTransactions = true;
this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.transactions[this.transactions.length - 1].txid)

View File

@ -14,7 +14,7 @@
<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 class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</div>
</div>
</div>
</div>

View File

@ -14,8 +14,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
blocks: Block[] = [];
blocksSubscription: Subscription;
interval: any;
trigger = 0;
arrowVisible = false;
arrowLeftPx = 30;
@ -35,8 +33,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.moveArrowToPosition();
});
this.interval = setInterval(() => this.trigger++, 10 * 1000);
}
ngOnChanges() {

View File

@ -11,7 +11,7 @@
<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><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</td>
<td>{{ block.tx_count }}</td>
<td>{{ block.size | bytes: 2 }}</td>
<td class="d-none d-md-block">

View File

@ -14,7 +14,6 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
blockSubscription: Subscription;
isLoading = true;
interval: any;
trigger = 0;
constructor(
private electrsApiService: ElectrsApiService,
@ -47,7 +46,6 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
});
this.loadInitialBlocks();
this.interval = window.setInterval(() => this.trigger++, 1000 * 60);
}
ngOnDestroy() {

View File

@ -10,6 +10,7 @@ export class TimeSinceComponent implements OnInit, OnDestroy {
trigger = 0;
@Input() time: number;
@Input() fastRender = false;
constructor(
private ref: ChangeDetectorRef
@ -19,7 +20,7 @@ export class TimeSinceComponent implements OnInit, OnDestroy {
this.interval = window.setInterval(() => {
this.trigger++;
this.ref.markForCheck();
}, 1000 * 60);
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnDestroy() {

View File

@ -53,6 +53,18 @@
<table class="table table-borderless table-striped">
<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>
<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>

View File

@ -7,6 +7,7 @@ import { of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-transaction',
@ -20,6 +21,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
conversions: any;
error: any = undefined;
latestBlock: Block;
transactionTime = -1;
rightPosition = 0;
blockDepth = 0;
@ -30,6 +32,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
private stateService: StateService,
private websocketService: WebsocketService,
private audioService: AudioService,
private apiService: ApiService,
) { }
ngOnInit() {
@ -55,6 +58,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
}
this.getTransactionTime();
},
(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() {
this.websocketService.startTrackTransaction('stop');
}

View File

@ -1,7 +1,12 @@
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
<div *ngIf="!transactionPage" class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/', tx.txid]" [state]="{ data: tx }">{{ tx.txid }}</a>
<div class="float-right">{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</div>
<div 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 class="header-bg box">
<div class="row">

View File

@ -11,7 +11,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TransactionsListComponent implements OnInit, OnChanges {
@Input() transactions: any[];
@Input() transactions: Transaction[];
@Input() showConfirmations = false;
@Input() transactionPage = false;

View File

@ -8,6 +8,7 @@ export interface Transaction {
vin: Vin[];
vout: Vout[];
status: Status;
firstSeen?: number;
}
export interface Recent {

View File

@ -1,5 +1,5 @@
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 { Observable } from 'rxjs';
@ -40,4 +40,12 @@ export class ApiService {
list1YStatistics$(): Observable<OptimizedMempoolStats[]> {
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 });
}
}