Generate mining basic pool ranking (sorted by block found) for a specified timeframe
This commit is contained in:
@@ -115,6 +115,11 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
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> {
|
||||
let esploraTransaction: IEsploraApi.Transaction = {
|
||||
txid: transaction.txid,
|
||||
|
||||
@@ -115,7 +115,7 @@ class Blocks {
|
||||
*/
|
||||
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined) : Promise<PoolTag> {
|
||||
if (txMinerInfo === undefined) {
|
||||
return poolsRepository.getUnknownPool();
|
||||
return await poolsRepository.$getUnknownPool();
|
||||
}
|
||||
|
||||
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() {
|
||||
let currentBlockHeight = await bitcoinApi.$getBlockHeightTip();
|
||||
let maxBlocks = 100; // tmp
|
||||
let maxBlocks = 1008*2; // tmp
|
||||
|
||||
while (currentBlockHeight-- > 0 && maxBlocks-- > 0) {
|
||||
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/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 + 'pools', routes.getPools)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
||||
|
||||
export interface PoolTag extends RowDataPacket {
|
||||
export interface PoolTag {
|
||||
id: number | null, // mysql row id
|
||||
name: string,
|
||||
link: string,
|
||||
regexes: string,
|
||||
addresses: string,
|
||||
regexes: string, // JSON array
|
||||
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 {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { IEsploraApi } from "../api/bitcoin/esplora-api.interface";
|
||||
import { BlockExtended, PoolTag } from "../mempool.interfaces";
|
||||
import { DB } from "../database";
|
||||
import logger from "../logger";
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
|
||||
export interface EmptyBlocks {
|
||||
emptyBlocks: number,
|
||||
poolId: number,
|
||||
}
|
||||
|
||||
class BlocksRepository {
|
||||
/**
|
||||
* Save indexed block data in the database
|
||||
* @param block
|
||||
* @param blockHash
|
||||
* @param coinbaseTxid
|
||||
* @param poolTag
|
||||
*/
|
||||
public async $saveBlockInDatabase(
|
||||
block: BlockExtended,
|
||||
@@ -26,7 +25,7 @@ class BlocksRepository {
|
||||
weight, tx_count, coinbase_raw, difficulty,
|
||||
pool_id, fees, fee_span, median_fee
|
||||
) VALUE (
|
||||
?, ?, ?, ?,
|
||||
?, ?, FROM_UNIXTIME(?), ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?
|
||||
)`;
|
||||
@@ -49,25 +48,49 @@ class BlocksRepository {
|
||||
/**
|
||||
* 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.
|
||||
* @param blockHeight
|
||||
* @returns
|
||||
*/
|
||||
public async $isBlockAlreadyIndexed(blockHeight: number) {
|
||||
const connection = await DB.pool.getConnection();
|
||||
let exists = false;
|
||||
|
||||
try {
|
||||
const query = `SELECT height from blocks where blocks.height = ${blockHeight}`;
|
||||
const [rows]: any[] = await connection.query(query);
|
||||
exists = rows.length === 1;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
logger.err('$isBlockAlreadyIndexed() error' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
const query = `SELECT height from blocks where blocks.height = ${blockHeight}`;
|
||||
const [rows]: any[] = await connection.query(query);
|
||||
exists = rows.length === 1;
|
||||
connection.release();
|
||||
|
||||
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();
|
||||
@@ -1,30 +1,43 @@
|
||||
import { FieldPacket } from "mysql2";
|
||||
import { DB } from "../database";
|
||||
import { PoolTag } from "../mempool.interfaces"
|
||||
import { PoolInfo, PoolTag } from "../mempool.interfaces"
|
||||
|
||||
class PoolsRepository {
|
||||
/**
|
||||
* Get all pools tagging info
|
||||
*/
|
||||
public async $getPools() : Promise<PoolTag[]> {
|
||||
public async $getPools(): Promise<PoolTag[]> {
|
||||
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();
|
||||
return rows;
|
||||
return <PoolTag[]>rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unknown pool tagging info
|
||||
*/
|
||||
public getUnknownPool(): PoolTag {
|
||||
return <PoolTag>{
|
||||
id: null,
|
||||
name: 'Unknown',
|
||||
link: 'rickroll?',
|
||||
regexes: "[]",
|
||||
addresses: "[]",
|
||||
};
|
||||
}
|
||||
public async $getUnknownPool(): Promise<PoolTag> {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const [rows] = await connection.query("SELECT * FROM pools where name = 'Unknown'");
|
||||
connection.release();
|
||||
return <PoolTag>rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -20,6 +20,7 @@ import { Common } from './api/common';
|
||||
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||
import elementsParser from './api/liquid/elements-parser';
|
||||
import icons from './api/liquid/icons';
|
||||
import miningStats from './api/mining';
|
||||
|
||||
class Routes {
|
||||
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) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||
|
||||
Reference in New Issue
Block a user