mirror of
https://github.com/mempool/mempool.git
synced 2025-01-18 05:12:35 +01:00
Added first seen on mempool transactions.
This commit is contained in:
parent
23a61a37fd
commit
4879036216
@ -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');
|
||||
|
@ -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)
|
||||
|
@ -33,6 +33,7 @@ export interface TransactionExtended extends Transaction {
|
||||
size: number;
|
||||
vsize: number;
|
||||
feePerVsize: number;
|
||||
firstSeen: number;
|
||||
}
|
||||
|
||||
export interface Prevout {
|
||||
|
@ -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();
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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">
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
||||
|
@ -8,6 +8,7 @@ export interface Transaction {
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
status: Status;
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface Recent {
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user