diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index ceedad2ed..bba043759 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -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 { 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'); diff --git a/backend/src/index.ts b/backend/src/index.ts index f9c3d6d1a..d66c535f0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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) diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index d7bf8b15b..1851a823d 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -33,6 +33,7 @@ export interface TransactionExtended extends Transaction { size: number; vsize: number; feePerVsize: number; + firstSeen: number; } export interface Prevout { diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 938592261..2b225c96a 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -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(); diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index c28b4885c..c0d7be0f2 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -79,12 +79,6 @@ -
- -
-
-

-
diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 5d12e0d9c..9bed46edd 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -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) diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 94e95c2b1..60d1ce035 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -14,7 +14,7 @@
{{ block.size | bytes: 2 }}
{{ block.tx_count }} transactions


-
{{ block.timestamp | timeSince : trigger }} ago
+
ago
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index d9dedd870..262ea0c6c 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -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() { diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.html b/frontend/src/app/components/latest-blocks/latest-blocks.component.html index fd52451c1..320172193 100644 --- a/frontend/src/app/components/latest-blocks/latest-blocks.component.html +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.html @@ -11,7 +11,7 @@ #{{ block.height }} {{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} - {{ block.timestamp | timeSince : trigger }} ago + ago {{ block.tx_count }} {{ block.size | bytes: 2 }} diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts index c52519870..99e0a2bc9 100644 --- a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts @@ -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() { diff --git a/frontend/src/app/components/time-since/time-since.component.ts b/frontend/src/app/components/time-since/time-since.component.ts index 223d2d7ed..9f6f8e62d 100644 --- a/frontend/src/app/components/time-since/time-since.component.ts +++ b/frontend/src/app/components/time-since/time-since.component.ts @@ -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() { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 36c75ea3d..5ed7cf9f9 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -53,6 +53,18 @@ + + + + + + + + + + + + diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 4682752c5..1ebad8f9d 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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'); } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index d2698c104..fcf95a0be 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -1,7 +1,12 @@
{{ tx.txid }} -
{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
+
+ {{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} + + ago + +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 522b0caf5..8389d1f51 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -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; diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 5cbc1b6cd..3b0230cd3 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -8,6 +8,7 @@ export interface Transaction { vin: Vin[]; vout: Vout[]; status: Status; + firstSeen?: number; } export interface Recent { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index f25dd5e54..7078aa551 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -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 { return this.httpClient.get(API_BASE_URL + '/statistics/1y'); } + + getTransactionTimes$(txIds: string[]): Observable { + let params = new HttpParams(); + txIds.forEach((txId: string) => { + params = params.append('txId[]', txId); + }); + return this.httpClient.get(API_BASE_URL + '/transaction-times', { params }); + } }
First seen ago
Fees {{ tx.fee | number }} sats ({{ conversions.USD * tx.fee / 100000000 | currency:'USD':'symbol':'1.2-2' }})