2022-01-18 09:37:04 +01:00
|
|
|
import { BlockExtended, PoolTag } from '../mempool.interfaces';
|
|
|
|
import { DB } from '../database';
|
|
|
|
import logger from '../logger';
|
2022-02-09 11:41:05 +01:00
|
|
|
import { Common } from '../api/common';
|
2022-01-06 11:59:33 +01:00
|
|
|
|
2022-01-05 07:41:14 +01:00
|
|
|
class BlocksRepository {
|
|
|
|
/**
|
|
|
|
* Save indexed block data in the database
|
|
|
|
*/
|
2022-02-08 07:47:43 +01:00
|
|
|
public async $saveBlockInDatabase(block: BlockExtended) {
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-01-05 07:41:14 +01:00
|
|
|
|
|
|
|
try {
|
|
|
|
const query = `INSERT INTO blocks(
|
2022-03-09 19:18:51 +01:00
|
|
|
height, hash, blockTimestamp, size,
|
|
|
|
weight, tx_count, coinbase_raw, difficulty,
|
|
|
|
pool_id, fees, fee_span, median_fee,
|
|
|
|
reward, version, bits, nonce,
|
2022-03-10 14:21:11 +01:00
|
|
|
merkle_root, previous_block_hash, avg_fee, avg_fee_rate
|
2022-01-05 07:41:14 +01:00
|
|
|
) VALUE (
|
2022-01-06 11:59:33 +01:00
|
|
|
?, ?, FROM_UNIXTIME(?), ?,
|
2022-01-05 07:41:14 +01:00
|
|
|
?, ?, ?, ?,
|
2022-02-08 07:47:43 +01:00
|
|
|
?, ?, ?, ?,
|
2022-02-16 07:19:16 +01:00
|
|
|
?, ?, ?, ?,
|
2022-03-10 14:21:11 +01:00
|
|
|
?, ?, ?, ?
|
2022-01-05 07:41:14 +01:00
|
|
|
)`;
|
|
|
|
|
|
|
|
const params: any[] = [
|
2022-02-04 04:51:45 +01:00
|
|
|
block.height,
|
2022-02-08 07:47:43 +01:00
|
|
|
block.id,
|
2022-02-04 04:51:45 +01:00
|
|
|
block.timestamp,
|
|
|
|
block.size,
|
|
|
|
block.weight,
|
|
|
|
block.tx_count,
|
2022-03-16 12:10:18 +01:00
|
|
|
block.extras.coinbaseRaw,
|
2022-02-04 04:51:45 +01:00
|
|
|
block.difficulty,
|
2022-02-16 07:19:16 +01:00
|
|
|
block.extras.pool?.id, // Should always be set to something
|
2022-03-09 19:18:51 +01:00
|
|
|
block.extras.totalFees,
|
|
|
|
JSON.stringify(block.extras.feeRange),
|
|
|
|
block.extras.medianFee,
|
|
|
|
block.extras.reward,
|
2022-02-16 07:19:16 +01:00
|
|
|
block.version,
|
|
|
|
block.bits,
|
|
|
|
block.nonce,
|
|
|
|
block.merkle_root,
|
2022-03-09 19:18:51 +01:00
|
|
|
block.previousblockhash,
|
|
|
|
block.extras.avgFee,
|
|
|
|
block.extras.avgFeeRate,
|
2022-01-05 07:41:14 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
await connection.query(query, params);
|
2022-03-06 16:44:09 +01:00
|
|
|
connection.release();
|
2022-02-05 07:50:57 +01:00
|
|
|
} catch (e: any) {
|
2022-03-06 16:44:09 +01:00
|
|
|
connection.release();
|
2022-02-05 07:50:57 +01:00
|
|
|
if (e.errno === 1062) { // ER_DUP_ENTRY
|
|
|
|
logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
|
|
|
|
} else {
|
2022-03-06 16:44:09 +01:00
|
|
|
connection.release();
|
2022-03-15 23:33:51 +01:00
|
|
|
logger.err('$saveBlockInDatabase() error: ' + (e instanceof Error ? e.message : e));
|
2022-03-06 16:44:09 +01:00
|
|
|
throw e;
|
2022-02-05 07:50:57 +01:00
|
|
|
}
|
2022-01-05 07:41:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-18 09:37:04 +01:00
|
|
|
/**
|
|
|
|
* Get all block height that have not been indexed between [startHeight, endHeight]
|
|
|
|
*/
|
|
|
|
public async $getMissingBlocksBetweenHeights(startHeight: number, endHeight: number): Promise<number[]> {
|
2022-01-24 09:43:11 +01:00
|
|
|
if (startHeight < endHeight) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows]: any[] = await connection.query(`
|
|
|
|
SELECT height
|
|
|
|
FROM blocks
|
|
|
|
WHERE height <= ? AND height >= ?
|
|
|
|
ORDER BY height DESC;
|
|
|
|
`, [startHeight, endHeight]);
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
const indexedBlockHeights: number[] = [];
|
|
|
|
rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
|
|
|
|
const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
|
|
|
|
const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
|
|
|
|
|
|
|
|
return missingBlocksHeights;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getMissingBlocksBetweenHeights() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-01-18 09:37:04 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 11:59:33 +01:00
|
|
|
/**
|
2022-02-09 11:41:05 +01:00
|
|
|
* Get empty blocks for one or all pools
|
2022-01-06 11:59:33 +01:00
|
|
|
*/
|
2022-03-08 16:55:49 +01:00
|
|
|
public async $countEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<any> {
|
2022-02-09 11:41:05 +01:00
|
|
|
interval = Common.getSqlInterval(interval);
|
|
|
|
|
|
|
|
const params: any[] = [];
|
2022-03-08 16:55:49 +01:00
|
|
|
let query = `SELECT count(height) as count, pools.id as poolId
|
2022-01-06 11:59:33 +01:00
|
|
|
FROM blocks
|
2022-03-08 16:55:49 +01:00
|
|
|
JOIN pools on pools.id = blocks.pool_id
|
2022-02-09 11:41:05 +01:00
|
|
|
WHERE tx_count = 1`;
|
|
|
|
|
|
|
|
if (poolId) {
|
|
|
|
query += ` AND pool_id = ?`;
|
|
|
|
params.push(poolId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (interval) {
|
|
|
|
query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
|
|
|
}
|
2022-01-25 10:33:46 +01:00
|
|
|
|
2022-03-08 16:55:49 +01:00
|
|
|
query += ` GROUP by pools.id`;
|
|
|
|
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows] = await connection.query(query, params);
|
|
|
|
connection.release();
|
|
|
|
|
2022-03-08 16:55:49 +01:00
|
|
|
return rows;
|
2022-03-06 16:44:09 +01:00
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-01-06 11:59:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get blocks count for a period
|
|
|
|
*/
|
2022-03-08 13:54:04 +01:00
|
|
|
public async $blockCount(poolId: number | null, interval: string | null = null): Promise<number> {
|
2022-02-09 11:41:05 +01:00
|
|
|
interval = Common.getSqlInterval(interval);
|
2022-01-25 10:33:46 +01:00
|
|
|
|
2022-02-09 11:41:05 +01:00
|
|
|
const params: any[] = [];
|
|
|
|
let query = `SELECT count(height) as blockCount
|
|
|
|
FROM blocks`;
|
|
|
|
|
|
|
|
if (poolId) {
|
|
|
|
query += ` WHERE pool_id = ?`;
|
|
|
|
params.push(poolId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (interval) {
|
|
|
|
if (poolId) {
|
|
|
|
query += ` AND`;
|
|
|
|
} else {
|
|
|
|
query += ` WHERE`;
|
|
|
|
}
|
|
|
|
query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
|
|
|
}
|
|
|
|
|
2022-02-11 11:12:52 +01:00
|
|
|
// logger.debug(query);
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows] = await connection.query(query, params);
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
return <number>rows[0].blockCount;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$blockCount() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-01-25 10:33:46 +01:00
|
|
|
}
|
|
|
|
|
2022-02-19 12:45:02 +01:00
|
|
|
/**
|
|
|
|
* Get blocks count between two dates
|
|
|
|
* @param poolId
|
|
|
|
* @param from - The oldest timestamp
|
|
|
|
* @param to - The newest timestamp
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise<number> {
|
|
|
|
const params: any[] = [];
|
|
|
|
let query = `SELECT
|
|
|
|
count(height) as blockCount,
|
|
|
|
max(height) as lastBlockHeight
|
|
|
|
FROM blocks`;
|
|
|
|
|
|
|
|
if (poolId) {
|
|
|
|
query += ` WHERE pool_id = ?`;
|
|
|
|
params.push(poolId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (poolId) {
|
|
|
|
query += ` AND`;
|
|
|
|
} else {
|
|
|
|
query += ` WHERE`;
|
|
|
|
}
|
2022-02-24 08:55:18 +01:00
|
|
|
query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`;
|
2022-02-19 12:45:02 +01:00
|
|
|
|
|
|
|
// logger.debug(query);
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows] = await connection.query(query, params);
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
return <number>rows[0];
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$blockCountBetweenTimestamp() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-02-19 12:45:02 +01:00
|
|
|
}
|
|
|
|
|
2022-01-25 10:33:46 +01:00
|
|
|
/**
|
|
|
|
* Get the oldest indexed block
|
|
|
|
*/
|
|
|
|
public async $oldestBlockTimestamp(): Promise<number> {
|
2022-02-22 07:50:14 +01:00
|
|
|
const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
|
2022-01-06 11:59:33 +01:00
|
|
|
FROM blocks
|
2022-01-25 10:33:46 +01:00
|
|
|
ORDER BY height
|
2022-02-09 11:41:05 +01:00
|
|
|
LIMIT 1;`;
|
|
|
|
|
2022-02-11 11:12:52 +01:00
|
|
|
// logger.debug(query);
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows]: any[] = await connection.query(query);
|
|
|
|
connection.release();
|
2022-01-06 11:59:33 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
if (rows.length <= 0) {
|
|
|
|
return -1;
|
|
|
|
}
|
2022-01-25 10:33:46 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
return <number>rows[0].blockTimestamp;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-01-06 11:59:33 +01:00
|
|
|
}
|
2022-02-08 07:47:43 +01:00
|
|
|
|
2022-02-08 10:28:53 +01:00
|
|
|
/**
|
|
|
|
* Get blocks mined by a specific mining pool
|
|
|
|
*/
|
2022-03-06 16:44:09 +01:00
|
|
|
public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise<object[]> {
|
2022-02-09 11:41:05 +01:00
|
|
|
const params: any[] = [];
|
2022-02-14 06:11:55 +01:00
|
|
|
let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
|
2022-02-08 10:28:53 +01:00
|
|
|
FROM blocks
|
2022-02-09 11:41:05 +01:00
|
|
|
WHERE pool_id = ?`;
|
|
|
|
params.push(poolId);
|
|
|
|
|
|
|
|
if (startHeight) {
|
|
|
|
query += ` AND height < ?`;
|
|
|
|
params.push(startHeight);
|
|
|
|
}
|
2022-02-08 10:28:53 +01:00
|
|
|
|
2022-02-09 11:41:05 +01:00
|
|
|
query += ` ORDER BY height DESC
|
|
|
|
LIMIT 10`;
|
|
|
|
|
2022-02-11 11:12:52 +01:00
|
|
|
// logger.debug(query);
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows] = await connection.query(query, params);
|
|
|
|
connection.release();
|
2022-02-08 10:28:53 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
for (const block of <object[]>rows) {
|
|
|
|
delete block['blockTimestamp'];
|
|
|
|
}
|
2022-02-09 11:41:05 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
return <object[]>rows;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-02-08 10:28:53 +01:00
|
|
|
}
|
|
|
|
|
2022-02-08 07:47:43 +01:00
|
|
|
/**
|
|
|
|
* Get one block by height
|
|
|
|
*/
|
2022-03-09 19:18:51 +01:00
|
|
|
public async $getBlockByHeight(height: number): Promise<object | null> {
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows]: any[] = await connection.query(`
|
2022-03-11 14:54:34 +01:00
|
|
|
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
|
|
|
|
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link,
|
|
|
|
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
|
|
|
|
previous_block_hash as previousblockhash
|
2022-03-06 16:44:09 +01:00
|
|
|
FROM blocks
|
|
|
|
JOIN pools ON blocks.pool_id = pools.id
|
|
|
|
WHERE height = ${height};
|
|
|
|
`);
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
if (rows.length <= 0) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-02-08 07:47:43 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
return rows[0];
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
2022-02-08 07:47:43 +01:00
|
|
|
}
|
|
|
|
}
|
2022-02-16 13:20:28 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return blocks difficulty
|
|
|
|
*/
|
2022-03-09 19:18:51 +01:00
|
|
|
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
|
2022-02-16 13:20:28 +01:00
|
|
|
interval = Common.getSqlInterval(interval);
|
|
|
|
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-02-16 13:20:28 +01:00
|
|
|
|
2022-02-22 14:53:47 +01:00
|
|
|
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
|
|
|
|
// Basically, using temporary user defined fields, we are able to extract all
|
|
|
|
// difficulty adjustments from the blocks tables.
|
|
|
|
// This allow use to avoid indexing it in another table.
|
|
|
|
let query = `
|
|
|
|
SELECT
|
|
|
|
*
|
|
|
|
FROM
|
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
|
|
|
|
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
|
|
|
|
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
|
|
|
|
) AS rn
|
|
|
|
FROM blocks YT
|
|
|
|
CROSS JOIN
|
|
|
|
(
|
|
|
|
SELECT @prevStatus := -1, @rn := 1
|
|
|
|
) AS var
|
|
|
|
`;
|
2022-02-16 13:20:28 +01:00
|
|
|
|
|
|
|
if (interval) {
|
|
|
|
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
|
|
|
}
|
|
|
|
|
2022-02-22 14:53:47 +01:00
|
|
|
query += `
|
|
|
|
ORDER BY YT.height
|
|
|
|
) AS t
|
|
|
|
WHERE t.rn = 1
|
|
|
|
ORDER BY t.height
|
|
|
|
`;
|
2022-02-16 13:20:28 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows]: any[] = await connection.query(query);
|
|
|
|
connection.release();
|
2022-02-16 13:20:28 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
for (let row of rows) {
|
|
|
|
delete row['rn'];
|
|
|
|
}
|
2022-02-24 08:55:18 +01:00
|
|
|
|
2022-03-06 16:44:09 +01:00
|
|
|
return rows;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-02-16 13:20:28 +01:00
|
|
|
}
|
2022-02-21 04:22:20 +01:00
|
|
|
|
|
|
|
public async $getOldestIndexedBlockHeight(): Promise<number> {
|
2022-03-12 14:47:33 +01:00
|
|
|
const connection = await DB.getConnection();
|
2022-03-06 16:44:09 +01:00
|
|
|
try {
|
|
|
|
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
|
|
|
|
connection.release();
|
|
|
|
|
|
|
|
return rows[0].minHeight;
|
|
|
|
} catch (e) {
|
|
|
|
connection.release();
|
|
|
|
logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e));
|
|
|
|
throw e;
|
|
|
|
}
|
2022-02-21 04:22:20 +01:00
|
|
|
}
|
2022-01-05 07:41:14 +01:00
|
|
|
}
|
|
|
|
|
2022-02-08 10:28:53 +01:00
|
|
|
export default new BlocksRepository();
|