Generate mining basic pool ranking (sorted by block found) for a specified timeframe

This commit is contained in:
nymkappa 2022-01-06 19:59:33 +09:00
parent 37031ec913
commit bfe9f99c35
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
16 changed files with 366 additions and 48 deletions

View file

@ -115,6 +115,11 @@ class BitcoinApi implements AbstractBitcoinApi {
return outSpends; return outSpends;
} }
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
}
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> { protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = { let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid, txid: transaction.txid,

View file

@ -115,7 +115,7 @@ class Blocks {
*/ */
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined) : Promise<PoolTag> { private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined) : Promise<PoolTag> {
if (txMinerInfo === undefined) { if (txMinerInfo === undefined) {
return poolsRepository.getUnknownPool(); return await poolsRepository.$getUnknownPool();
} }
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig); const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
@ -139,7 +139,7 @@ class Blocks {
} }
} }
return poolsRepository.getUnknownPool(); return await poolsRepository.$getUnknownPool();
} }
/** /**
@ -147,7 +147,7 @@ class Blocks {
*/ */
public async $generateBlockDatabase() { public async $generateBlockDatabase() {
let currentBlockHeight = await bitcoinApi.$getBlockHeightTip(); let currentBlockHeight = await bitcoinApi.$getBlockHeightTip();
let maxBlocks = 100; // tmp let maxBlocks = 1008*2; // tmp
while (currentBlockHeight-- > 0 && maxBlocks-- > 0) { while (currentBlockHeight-- > 0 && maxBlocks-- > 0) {
if (await blocksRepository.$isBlockAlreadyIndexed(currentBlockHeight)) { if (await blocksRepository.$isBlockAlreadyIndexed(currentBlockHeight)) {

53
backend/src/api/mining.ts Normal file
View file

@ -0,0 +1,53 @@
import { PoolInfo, PoolStats } from "../mempool.interfaces";
import BlocksRepository, { EmptyBlocks } from "../repositories/BlocksRepository";
import PoolsRepository from "../repositories/PoolsRepository";
import bitcoinClient from "./bitcoin/bitcoin-client";
import BitcoinApi from "./bitcoin/bitcoin-api";
class Mining {
private bitcoinApi: BitcoinApi;
constructor() {
this.bitcoinApi = new BitcoinApi(bitcoinClient);
}
/**
* Generate high level overview of the pool ranks and general stats
*/
public async $getPoolsStats(interval: string = "100 YEAR") : Promise<object> {
let poolsStatistics = {};
const lastBlockHashrate = await this.bitcoinApi.$getEstimatedHashrate(717960);
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
const blockCount: number = await BlocksRepository.$blockCount(interval);
const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$countEmptyBlocks(interval);
let poolsStats: PoolStats[] = [];
let rank = 1;
poolsInfo.forEach((poolInfo: PoolInfo) => {
let poolStat: PoolStats = {
poolId: poolInfo.poolId, // mysql row id
name: poolInfo.name,
link: poolInfo.link,
blockCount: poolInfo.blockCount,
rank: rank++,
emptyBlocks: 0,
}
for (let i = 0; i < emptyBlocks.length; ++i) {
if (emptyBlocks[i].poolId === poolInfo.poolId) {
poolStat.emptyBlocks++;
}
}
poolsStats.push(poolStat);
})
poolsStatistics["blockCount"] = blockCount;
poolsStatistics["poolsStats"] = poolsStats;
poolsStatistics["lastEstimatedHashrate"] = lastBlockHashrate;
return poolsStatistics;
}
}
export default new Mining();

View file

@ -261,6 +261,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'pools', routes.getPools)
; ;
} }

View file

@ -1,11 +1,23 @@
import { RowDataPacket } from 'mysql2';
import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
export interface PoolTag extends RowDataPacket { export interface PoolTag {
id: number | null, // mysql row id
name: string, name: string,
link: string, link: string,
regexes: string, regexes: string, // JSON array
addresses: string, addresses: string, // JSON array
}
export interface PoolInfo {
poolId: number, // mysql row id
name: string,
link: string,
blockCount: number,
}
export interface PoolStats extends PoolInfo {
rank: number,
emptyBlocks: number,
} }
export interface MempoolBlock { export interface MempoolBlock {

View file

@ -1,16 +1,15 @@
import { IEsploraApi } from "../api/bitcoin/esplora-api.interface";
import { BlockExtended, PoolTag } from "../mempool.interfaces"; import { BlockExtended, PoolTag } from "../mempool.interfaces";
import { DB } from "../database"; import { DB } from "../database";
import logger from "../logger"; import logger from "../logger";
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
export interface EmptyBlocks {
emptyBlocks: number,
poolId: number,
}
class BlocksRepository { class BlocksRepository {
/** /**
* Save indexed block data in the database * Save indexed block data in the database
* @param block
* @param blockHash
* @param coinbaseTxid
* @param poolTag
*/ */
public async $saveBlockInDatabase( public async $saveBlockInDatabase(
block: BlockExtended, block: BlockExtended,
@ -26,7 +25,7 @@ class BlocksRepository {
weight, tx_count, coinbase_raw, difficulty, weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee pool_id, fees, fee_span, median_fee
) VALUE ( ) VALUE (
?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ? ?, ?, ?, ?
)`; )`;
@ -49,25 +48,49 @@ class BlocksRepository {
/** /**
* Check if a block has already been indexed in the database. Query the databse directly. * Check if a block has already been indexed in the database. Query the databse directly.
* This can be cached/optimized if required later on to avoid too many db queries. * This can be cached/optimized if required later on to avoid too many db queries.
* @param blockHeight
* @returns
*/ */
public async $isBlockAlreadyIndexed(blockHeight: number) { public async $isBlockAlreadyIndexed(blockHeight: number) {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
let exists = false; let exists = false;
try {
const query = `SELECT height from blocks where blocks.height = ${blockHeight}`; const query = `SELECT height from blocks where blocks.height = ${blockHeight}`;
const [rows]: any[] = await connection.query(query); const [rows]: any[] = await connection.query(query);
exists = rows.length === 1; exists = rows.length === 1;
} catch (e) {
console.log(e);
logger.err('$isBlockAlreadyIndexed() error' + (e instanceof Error ? e.message : e));
}
connection.release(); connection.release();
return exists; return exists;
} }
/**
* Count empty blocks for all pools
*/
public async $countEmptyBlocks(interval: string = "100 YEAR") : Promise<EmptyBlocks[]> {
const connection = await DB.pool.getConnection();
const [rows] = await connection.query(`
SELECT pool_id as poolId
FROM blocks
WHERE timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND tx_count = 1;
`);
connection.release();
return <EmptyBlocks[]>rows;
}
/**
* Get blocks count for a period
*/
public async $blockCount(interval: string = "100 YEAR") : Promise<number> {
const connection = await DB.pool.getConnection();
const [rows] = await connection.query(`
SELECT count(height) as blockCount
FROM blocks
WHERE timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW();
`);
connection.release();
return <number>rows[0].blockCount;
}
} }
export default new BlocksRepository(); export default new BlocksRepository();

View file

@ -1,6 +1,5 @@
import { FieldPacket } from "mysql2";
import { DB } from "../database"; import { DB } from "../database";
import { PoolTag } from "../mempool.interfaces" import { PoolInfo, PoolTag } from "../mempool.interfaces"
class PoolsRepository { class PoolsRepository {
/** /**
@ -8,22 +7,36 @@ class PoolsRepository {
*/ */
public async $getPools(): Promise<PoolTag[]> { public async $getPools(): Promise<PoolTag[]> {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: [PoolTag[], FieldPacket[]] = await connection.query("SELECT * FROM pools;"); const [rows] = await connection.query("SELECT * FROM pools;");
connection.release(); connection.release();
return rows; return <PoolTag[]>rows;
} }
/** /**
* Get unknown pool tagging info * Get unknown pool tagging info
*/ */
public getUnknownPool(): PoolTag { public async $getUnknownPool(): Promise<PoolTag> {
return <PoolTag>{ const connection = await DB.pool.getConnection();
id: null, const [rows] = await connection.query("SELECT * FROM pools where name = 'Unknown'");
name: 'Unknown', connection.release();
link: 'rickroll?', return <PoolTag>rows[0];
regexes: "[]", }
addresses: "[]",
}; /**
* Get basic pool info and block count
*/
public async $getPoolsInfo(interval: string = "100 YEARS"): Promise<PoolInfo[]> {
const connection = await DB.pool.getConnection();
const [rows] = await connection.query(`
SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link
FROM blocks
JOIN pools on pools.id = pool_id
WHERE timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
GROUP BY pool_id
ORDER BY COUNT(height) DESC;
`);
connection.release();
return <PoolInfo[]>rows;
} }
} }

View file

@ -20,6 +20,7 @@ import { Common } from './api/common';
import bitcoinClient from './api/bitcoin/bitcoin-client'; import bitcoinClient from './api/bitcoin/bitcoin-client';
import elementsParser from './api/liquid/elements-parser'; import elementsParser from './api/liquid/elements-parser';
import icons from './api/liquid/icons'; import icons from './api/liquid/icons';
import miningStats from './api/mining';
class Routes { class Routes {
constructor() {} constructor() {}
@ -531,6 +532,15 @@ class Routes {
} }
} }
public async getPools(req: Request, res: Response) {
try {
let stats = await miningStats.$getPoolsStats(req.query.interval as string);
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlock(req: Request, res: Response) { public async getBlock(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getBlock(req.params.hash); const result = await bitcoinApi.$getBlock(req.params.hash);

View file

@ -6,7 +6,6 @@ import * as express from 'express';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as domino from 'domino'; import * as domino from 'domino';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { join } from 'path'; import { join } from 'path';
import { AppServerModule } from './src/main.server'; import { AppServerModule } from './src/main.server';
@ -66,6 +65,7 @@ export function app(locale: string): express.Express {
server.get('/mempool-block/*', getLocalizedSSR(indexHtml)); server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/address/*', getLocalizedSSR(indexHtml)); server.get('/address/*', getLocalizedSSR(indexHtml));
server.get('/blocks', getLocalizedSSR(indexHtml)); server.get('/blocks', getLocalizedSSR(indexHtml));
server.get('/pools', getLocalizedSSR(indexHtml));
server.get('/graphs', getLocalizedSSR(indexHtml)); server.get('/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid', getLocalizedSSR(indexHtml)); server.get('/liquid', getLocalizedSSR(indexHtml));
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml)); server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));

View file

@ -22,6 +22,7 @@ import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-mast
import { SponsorComponent } from './components/sponsor/sponsor.component'; import { SponsorComponent } from './components/sponsor/sponsor.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
let routes: Routes = [ let routes: Routes = [
{ {
@ -58,6 +59,10 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: LatestBlocksComponent, component: LatestBlocksComponent,
}, },
{
path: 'pools',
component: PoolRankingComponent,
},
{ {
path: 'graphs', path: 'graphs',
component: StatisticsComponent, component: StatisticsComponent,

View file

@ -37,6 +37,7 @@ import { IncomingTransactionsGraphComponent } from './components/incoming-transa
import { TimeSpanComponent } from './components/time-span/time-span.component'; import { TimeSpanComponent } from './components/time-span/time-span.component';
import { SeoService } from './services/seo.service'; import { SeoService } from './services/seo.service';
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component'; import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
import { AssetComponent } from './components/asset/asset.component'; import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './assets/assets.component'; import { AssetsComponent } from './assets/assets.component';
@ -91,6 +92,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
FeeDistributionGraphComponent, FeeDistributionGraphComponent,
IncomingTransactionsGraphComponent, IncomingTransactionsGraphComponent,
MempoolGraphComponent, MempoolGraphComponent,
PoolRankingComponent,
LbtcPegsGraphComponent, LbtcPegsGraphComponent,
AssetComponent, AssetComponent,
AssetsComponent, AssetsComponent,

View file

@ -0,0 +1,68 @@
<div class="container-xl">
<div class="card-header">
<i class="fa fa-area-chart"></i> <span i18n="mining.pools-by-vBytes">Pools</span>
<form [formGroup]="radioGroupForm" class="formRadioGroup" (click)="savePoolsPreference()">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1d'" [routerLink]="['/pools' | relativeUrl]" fragment="1d"> 1D
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'3d'" [routerLink]="['/pools' | relativeUrl]" fragment="3d"> 3D
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1w'" [routerLink]="['/pools' | relativeUrl]" fragment="1w"> 1W
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1m'" [routerLink]="['/pools' | relativeUrl]" fragment="1m"> 1M
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'3m'" [routerLink]="['/pools' | relativeUrl]" fragment="3m"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/pools' | relativeUrl]" fragment="6m"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/pools' | relativeUrl]" fragment="1y"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'2y'" [routerLink]="['/pools' | relativeUrl]" fragment="2y"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'3y'" [routerLink]="['/pools' | relativeUrl]" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" [routerLink]="['/pools' | relativeUrl]" fragment="all"> ALL
</label>
</div>
</form>
</div>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50">
<thead>
<th i18n="latest-blocks.height">Rank</th>
<th class="d-none d-md-block"i18n="latest-blocks.timestamp">Name</th>
<th i18n="latest-blocks.timestamp">Hashrate</th>
<th class="d-none d-md-block" i18n="latest-blocks.mined">Block Count (share)</th>
<th i18n="latest-blocks.transactions">Empty Blocks (ratio)</th>
</thead>
<tbody *ngIf="pools$ | async as pools">
<tr>
<td>-</td>
<td>All miners</td>
<td>{{ pools["lastEstimatedHashrate"]}} PH/s</td>
<td>{{ pools["blockCount"] }}</td>
<td>{{ pools["totalEmptyBlock"] }} ({{ pools["totalEmptyBlockRatio"] }}%)</td>
</tr>
<ng-template ngFor let-pool [ngForOf]="pools['poolsStats']">
<tr>
<td>{{ pool.rank }}</td>
<td><a href="{{ pool.link }}">{{ pool.name }}</a></td>
<td>{{ pool.lastEstimatedHashrate }} PH/s</td>
<td>{{ pool.blockCount }} ({{ pool.share }}%)</td>
<td>{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr>
</ng-template>
</tbody>
</table>
</div>

View file

@ -0,0 +1,103 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { merge, Observable, ObservableInput, of } from 'rxjs';
import { map, share, switchMap, tap } from 'rxjs/operators';
import { PoolsStats } from 'src/app/interfaces/node-api.interface';
import { StorageService } from 'src/app/services/storage.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-pool-ranking',
templateUrl: './pool-ranking.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PoolRankingComponent implements OnInit {
pools$: Observable<object>
radioGroupForm: FormGroup;
poolsWindowPreference: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private storageService: StorageService,
) { }
ngOnInit(): void {
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference').trim() : '2h';
this.radioGroupForm = this.formBuilder.group({
dateSpan: this.poolsWindowPreference
});
// Setup datespan triggers
this.route.fragment.subscribe((fragment) => {
if (['1d', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
merge(of(''), this.radioGroupForm.controls.dateSpan.valueChanges)
.pipe(switchMap(() => this.onDateSpanChanged()))
.subscribe((pools: any) => {
console.log(pools);
});
// Fetch initial mining pool data
this.onDateSpanChanged();
}
ngOnChanges() {
}
rendered() {
}
savePoolsPreference() {
this.storageService.setValue('poolsWindowPreference', this.radioGroupForm.controls.dateSpan.value);
this.poolsWindowPreference = this.radioGroupForm.controls.dateSpan.value;
}
onDateSpanChanged(): ObservableInput<any> {
let interval: string;
console.log(this.poolsWindowPreference);
switch (this.poolsWindowPreference) {
case '1d': interval = '1 DAY'; break;
case '3d': interval = '3 DAY'; break;
case '1w': interval = '1 WEEK'; break;
case '1m': interval = '1 MONTH'; break;
case '3m': interval = '3 MONTH'; break;
case '6m': interval = '6 MONTH'; break;
case '1y': interval = '1 YEAR'; break;
case '2y': interval = '2 YEAR'; break;
case '3y': interval = '3 YEAR'; break;
case 'all': interval = '1000 YEAR'; break;
}
this.pools$ = this.apiService.listPools$(interval).pipe(map(res => this.computeMiningStats(res)));
return this.pools$;
}
computeMiningStats(stats: PoolsStats) {
const totalEmptyBlock = Object.values(stats.poolsStats).reduce((prev, cur) => {
return prev + cur.emptyBlocks;
}, 0);
const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2);
const poolsStats = stats.poolsStats.map((poolStat) => {
return {
share: (poolStat.blockCount / stats.blockCount * 100).toFixed(2),
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / Math.pow(10, 15)).toFixed(2),
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
...poolStat
}
});
return {
lastEstimatedHashrate: (stats.lastEstimatedHashrate / Math.pow(10, 15)).toFixed(2),
blockCount: stats.blockCount,
totalEmptyBlock: totalEmptyBlock,
totalEmptyBlockRatio: totalEmptyBlockRatio,
poolsStats: poolsStats,
}
}
}

View file

@ -51,3 +51,16 @@ export interface LiquidPegs {
} }
export interface ITranslators { [language: string]: string; } export interface ITranslators { [language: string]: string; }
export interface PoolsStats {
poolsStats: {
pooldId: number,
name: string,
link: string,
blockCount: number,
emptyBlocks: number,
rank: number,
}[],
blockCount: number,
lastEstimatedHashrate: number,
}

View file

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators } from '../interfaces/node-api.interface'; import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { WebsocketResponse } from '../interfaces/websocket.interface'; import { WebsocketResponse } from '../interfaces/websocket.interface';
@ -120,4 +120,9 @@ export class ApiService {
postTransaction$(hexPayload: string): Observable<any> { postTransaction$(hexPayload: string): Observable<any> {
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
} }
listPools$(interval: string) : Observable<PoolsStats> {
const params = new HttpParams().set('interval', interval);
return this.httpClient.get<PoolsStats>(this.apiBaseUrl + this.apiBasePath + '/api/v1/pools', {params});
}
} }

View file

@ -6,16 +6,21 @@ import { Router, ActivatedRoute } from '@angular/router';
}) })
export class StorageService { export class StorageService {
constructor(private router: Router, private route: ActivatedRoute) { constructor(private router: Router, private route: ActivatedRoute) {
let graphWindowPreference: string = this.getValue('graphWindowPreference'); this.setDefaultValueIfNeeded('graphWindowPreference', '2h');
if (graphWindowPreference === null) { // First visit to mempool.space this.setDefaultValueIfNeeded('poolsWindowPreference', '1d');
if (this.router.url.includes("graphs")) {
this.setValue('graphWindowPreference', this.route.snapshot.fragment ? this.route.snapshot.fragment : "2h");
} else {
this.setValue('graphWindowPreference', "2h");
} }
} else if (this.router.url.includes("graphs")) { // Visit a different graphs#fragment from last visit
setDefaultValueIfNeeded(key: string, defaultValue: string) {
let graphWindowPreference: string = this.getValue(key);
if (graphWindowPreference === null) { // First visit to mempool.space
if (this.router.url.includes("graphs") || this.router.url.includes("pools")) {
this.setValue(key, this.route.snapshot.fragment ? this.route.snapshot.fragment : defaultValue);
} else {
this.setValue(key, defaultValue);
}
} else if (this.router.url.includes("graphs") || this.router.url.includes("pools")) { // Visit a different graphs#fragment from last visit
if (this.route.snapshot.fragment !== null && graphWindowPreference !== this.route.snapshot.fragment) { if (this.route.snapshot.fragment !== null && graphWindowPreference !== this.route.snapshot.fragment) {
this.setValue('graphWindowPreference', this.route.snapshot.fragment); this.setValue(key, this.route.snapshot.fragment);
} }
} }
} }