mirror of
https://github.com/mempool/mempool.git
synced 2025-03-15 04:11:48 +01:00
Generate mining basic pool ranking (sorted by block found) for a specified timeframe
This commit is contained in:
parent
37031ec913
commit
bfe9f99c35
16 changed files with 366 additions and 48 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
53
backend/src/api/mining.ts
Normal 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();
|
|
@ -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)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
|
@ -1,30 +1,43 @@
|
||||||
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 {
|
||||||
/**
|
/**
|
||||||
* Get all pools tagging info
|
* Get all pools tagging info
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PoolsRepository();
|
export default new PoolsRepository();
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,18 +6,23 @@ 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');
|
||||||
|
this.setDefaultValueIfNeeded('poolsWindowPreference', '1d');
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultValueIfNeeded(key: string, defaultValue: string) {
|
||||||
|
let graphWindowPreference: string = this.getValue(key);
|
||||||
if (graphWindowPreference === null) { // First visit to mempool.space
|
if (graphWindowPreference === null) { // First visit to mempool.space
|
||||||
if (this.router.url.includes("graphs")) {
|
if (this.router.url.includes("graphs") || this.router.url.includes("pools")) {
|
||||||
this.setValue('graphWindowPreference', this.route.snapshot.fragment ? this.route.snapshot.fragment : "2h");
|
this.setValue(key, this.route.snapshot.fragment ? this.route.snapshot.fragment : defaultValue);
|
||||||
} else {
|
} else {
|
||||||
this.setValue('graphWindowPreference', "2h");
|
this.setValue(key, defaultValue);
|
||||||
}
|
}
|
||||||
} else if (this.router.url.includes("graphs")) { // Visit a different graphs#fragment from last visit
|
} 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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue(key: string): string {
|
getValue(key: string): string {
|
||||||
|
|
Loading…
Add table
Reference in a new issue