Bisq statistics page.
This commit is contained in:
		
							parent
							
								
									87abfc38cb
								
							
						
					
					
						commit
						b7376fbd8d
					
				@ -14,7 +14,7 @@
 | 
			
		||||
  "TX_PER_SECOND_SPAN_SECONDS": 150,
 | 
			
		||||
  "ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
 | 
			
		||||
  "BISQ_ENABLED": false,
 | 
			
		||||
  "BSQ_BLOCKS_DATA_PATH": "/mempool/data/all/blocks.json",
 | 
			
		||||
  "BSQ_BLOCKS_DATA_PATH": "/bisq/data/all/blocks.json",
 | 
			
		||||
  "SSL": false,
 | 
			
		||||
  "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
 | 
			
		||||
  "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import { BisqBlocks, BisqBlock, BisqTransaction } from '../interfaces';
 | 
			
		||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class Bisq {
 | 
			
		||||
  private blocks: BisqBlock[] = [];
 | 
			
		||||
@ -8,6 +8,13 @@ class Bisq {
 | 
			
		||||
  private transactionIndex: { [txId: string]: BisqTransaction } = {};
 | 
			
		||||
  private blockIndex: { [hash: string]: BisqBlock } = {};
 | 
			
		||||
  private addressIndex: { [address: string]: BisqTransaction[] } = {};
 | 
			
		||||
  private stats: BisqStats = {
 | 
			
		||||
    minted: 0,
 | 
			
		||||
    burnt: 0,
 | 
			
		||||
    addresses: 0,
 | 
			
		||||
    unspent_txos: 0,
 | 
			
		||||
    spent_txos: 0,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
@ -48,11 +55,16 @@ class Bisq {
 | 
			
		||||
    return [this.blocks.slice(start, length + start), this.blocks.length];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStats(): BisqStats {
 | 
			
		||||
    return this.stats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async loadBisqDumpFile(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const data = await this.loadData();
 | 
			
		||||
      await this.loadBisqBlocksDump(data);
 | 
			
		||||
      this.buildIndex();
 | 
			
		||||
      this.calculateStats();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('loadBisqDumpFile() error.', e.message);
 | 
			
		||||
    }
 | 
			
		||||
@ -101,6 +113,38 @@ class Bisq {
 | 
			
		||||
    console.log('Bisq data index rebuilt in ' + time + ' ms');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateStats() {
 | 
			
		||||
    let minted = 0;
 | 
			
		||||
    let burned = 0;
 | 
			
		||||
    let unspent = 0;
 | 
			
		||||
    let spent = 0;
 | 
			
		||||
 | 
			
		||||
    this.transactions.forEach((tx) => {
 | 
			
		||||
      tx.outputs.forEach((output) => {
 | 
			
		||||
        if (output.opReturn) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) {
 | 
			
		||||
          minted += output.bsqAmount;
 | 
			
		||||
        }
 | 
			
		||||
        if (output.isUnspent) {
 | 
			
		||||
          unspent++;
 | 
			
		||||
        } else {
 | 
			
		||||
          spent++;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      burned += tx['burntFee'];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.stats = {
 | 
			
		||||
      addresses: Object.keys(this.addressIndex).length,
 | 
			
		||||
      minted: minted,
 | 
			
		||||
      burnt: burned,
 | 
			
		||||
      spent_txos: spent,
 | 
			
		||||
      unspent_txos: unspent,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async loadBisqBlocksDump(cacheData: string): Promise<void> {
 | 
			
		||||
    const start = new Date().getTime();
 | 
			
		||||
    if (cacheData && cacheData.length !== 0) {
 | 
			
		||||
 | 
			
		||||
@ -92,6 +92,7 @@ class Server {
 | 
			
		||||
 | 
			
		||||
    if (config.BISQ_ENABLED) {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.API_ENDPOINT + 'bisq/stats', routes.getBisqStats)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
 | 
			
		||||
 | 
			
		||||
@ -259,6 +259,14 @@ export interface BisqTransaction {
 | 
			
		||||
  unlockBlockHeight: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BisqStats {
 | 
			
		||||
  minted: number;
 | 
			
		||||
  burnt: number;
 | 
			
		||||
  addresses: number;
 | 
			
		||||
  unspent_txos: number;
 | 
			
		||||
  spent_txos: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BisqInput {
 | 
			
		||||
  spendingTxOutputIndex: number;
 | 
			
		||||
  spendingTxId: string;
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,11 @@ class Routes {
 | 
			
		||||
    res.send(backendInfo.getBackendInfo());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getBisqStats(req: Request, res: Response) {
 | 
			
		||||
    const result = bisq.getStats();
 | 
			
		||||
    res.send(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getBisqTransaction(req: Request, res: Response) {
 | 
			
		||||
    const result = bisq.getTransaction(req.params.txId);
 | 
			
		||||
    if (result) {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpResponse } from '@angular/common/http';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { BisqTransaction, BisqBlock } from './bisq.interfaces';
 | 
			
		||||
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
 | 
			
		||||
 | 
			
		||||
const API_BASE_URL = '/api/v1';
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,10 @@ export class BisqApiService {
 | 
			
		||||
    private httpClient: HttpClient,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  getStats$(): Observable<BisqStats> {
 | 
			
		||||
    return this.httpClient.get<BisqStats>(API_BASE_URL + '/bisq/stats');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTransaction$(txId: string): Observable<BisqTransaction> {
 | 
			
		||||
    return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/bisq/tx/' + txId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										85
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <h1 style="float: left;">BSQ Statistics</h1>
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <table class="table table-borderless table-striped">
 | 
			
		||||
    <thead>
 | 
			
		||||
      <th>Property</th>
 | 
			
		||||
      <th>Value</th>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody *ngIf="!isLoading; else loadingTemplate">
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td class="td-width">Existing amount</td>
 | 
			
		||||
        <td>{{ (stats.minted - stats.burnt) / 100 | number: '1.2-2' }} BSQ</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Minted amount</td>
 | 
			
		||||
        <td>{{ stats.minted | number: '1.2-2' }} BSQ</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Burnt amount</td>
 | 
			
		||||
        <td>{{ stats.burnt | number: '1.2-2' }} BSQ</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Addresses</td>
 | 
			
		||||
        <td>{{ stats.addresses | number }}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Unspent TXOs</td>
 | 
			
		||||
        <td>{{ stats.unspent_txos | number }}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Spent TXOs</td>
 | 
			
		||||
        <td>{{ stats.spent_txos | number }}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Price</td>
 | 
			
		||||
        <td></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <td>Market cap</td>
 | 
			
		||||
        <td></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #loadingTemplate>
 | 
			
		||||
  <tbody>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td class="td-width">Existing amount</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Minted amount</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Burnt amount</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Addresses</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Unspent TXOs</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Spent TXOs</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Price</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Market cap</td>
 | 
			
		||||
      <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
  </tbody>
 | 
			
		||||
</ng-template>
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
.td-width {
 | 
			
		||||
  width: 300px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767.98px) {
 | 
			
		||||
	.td-width {
 | 
			
		||||
		width: 175px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.skeleton-loader {
 | 
			
		||||
	width: 200px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { BisqApiService } from '../bisq-api.service';
 | 
			
		||||
import { BisqStats } from '../bisq.interfaces';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-bisq-stats',
 | 
			
		||||
  templateUrl: './bisq-stats.component.html',
 | 
			
		||||
  styleUrls: ['./bisq-stats.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class BisqStatsComponent implements OnInit {
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  stats: BisqStats;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private bisqApiService: BisqApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle('BSQ Statistics', false);
 | 
			
		||||
 | 
			
		||||
    this.bisqApiService.getStats$()
 | 
			
		||||
      .subscribe((stats) => {
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        this.stats = stats;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -59,6 +59,14 @@ export interface BisqOutput {
 | 
			
		||||
  opReturn?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BisqStats {
 | 
			
		||||
  minted: number;
 | 
			
		||||
  burnt: number;
 | 
			
		||||
  addresses: number;
 | 
			
		||||
  unspent_txos: number;
 | 
			
		||||
  spent_txos: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface BisqScriptPubKey {
 | 
			
		||||
  addresses: string[];
 | 
			
		||||
  asm: string;
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
 | 
			
		||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
 | 
			
		||||
import { BisqApiService } from './bisq-api.service';
 | 
			
		||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
 | 
			
		||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -29,6 +30,7 @@ import { BisqAddressComponent } from './bisq-address/bisq-address.component';
 | 
			
		||||
    BisqBlocksComponent,
 | 
			
		||||
    BisqExplorerComponent,
 | 
			
		||||
    BisqAddressComponent,
 | 
			
		||||
    BisqStatsComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { BisqBlockComponent } from './bisq-block/bisq-block.component';
 | 
			
		||||
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
 | 
			
		||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
 | 
			
		||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
 | 
			
		||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -35,6 +36,10 @@ const routes: Routes = [
 | 
			
		||||
        path: 'address/:id',
 | 
			
		||||
        component: BisqAddressComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'stats',
 | 
			
		||||
        component: BisqStatsComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        component: AboutComponent,
 | 
			
		||||
 | 
			
		||||
@ -26,20 +26,25 @@
 | 
			
		||||
  
 | 
			
		||||
  <div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
 | 
			
		||||
    <ul class="navbar-nav mr-auto {{ network }}">
 | 
			
		||||
      <ng-template [ngIf]="network === 'bisq'">
 | 
			
		||||
      <ng-template [ngIf]="network === 'bisq'" [ngIfElse]="notBisq">
 | 
			
		||||
        <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
 | 
			
		||||
          <a class="nav-link" [routerLink]="['/bisq']" (click)="collapse()">Transactions</a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
          <a class="nav-link" [routerLink]="['/bisq/blocks']" (click)="collapse()">Blocks</a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
          <a class="nav-link" [routerLink]="['/bisq/stats']" (click)="collapse()">Stats</a>
 | 
			
		||||
        </li>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <ng-template #notBisq>
 | 
			
		||||
        <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
          <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()">Graphs</a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
          <a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()">TV view  <img src="./resources/expand.png" width="15"/></a>
 | 
			
		||||
        </li>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <li *ngIf="network !== 'bisq'" class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()">Graphs</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li *ngIf="network !== 'bisq'" class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()">TV view  <img src="./resources/expand.png" width="15"/></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li *ngIf="network === 'liquid'" class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['liquid/assets']" (click)="collapse()">Assets</a>
 | 
			
		||||
      </li>
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ export class QrcodeComponent implements AfterViewInit {
 | 
			
		||||
      address.toUpperCase();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    QRCode.toCanvas(this.canvas.nativeElement, 'bitcoin:' + address, opts, (error: any) => {
 | 
			
		||||
    QRCode.toCanvas(this.canvas.nativeElement, address, opts, (error: any) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
         console.error(error);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user