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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user