Merge branch 'master' into regtest-1

This commit is contained in:
Antoni Spaanderman 2022-03-08 19:45:03 +01:00
commit d179a563e4
No known key found for this signature in database
GPG Key ID: AE0B68E552E5DF8C
42 changed files with 1109 additions and 391 deletions

View File

@ -89,19 +89,19 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
$getAddressPrefix(prefix: string): string[] { $getAddressPrefix(prefix: string): string[] {
const found: string[] = []; const found: { [address: string]: string } = {};
const mp = mempool.getMempool(); const mp = mempool.getMempool();
for (const tx in mp) { for (const tx in mp) {
for (const vout of mp[tx].vout) { for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) { if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
found.push(vout.scriptpubkey_address); found[vout.scriptpubkey_address] = '';
if (found.length >= 10) { if (Object.keys(found).length >= 10) {
return found; return Object.keys(found);
} }
} }
} }
} }
return found; return Object.keys(found);
} }
$sendRawTransaction(rawTransaction: string): Promise<string> { $sendRawTransaction(rawTransaction: string): Promise<string> {

View File

@ -171,7 +171,7 @@ class Blocks {
} }
/** /**
* Index all blocks metadata for the mining dashboard * [INDEXING] Index all blocks metadata for the mining dashboard
*/ */
public async $generateBlockDatabase() { public async $generateBlockDatabase() {
if (this.blockIndexingStarted) { if (this.blockIndexingStarted) {
@ -218,31 +218,28 @@ class Blocks {
if (blockHeight < lastBlockToIndex) { if (blockHeight < lastBlockToIndex) {
break; break;
} }
try { ++indexedThisRun;
++indexedThisRun; if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) {
if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) { const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
const progress = Math.round(totaIndexed / indexingBlockAmount * 100); const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds);
const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds); logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${elapsedSeconds} seconds | left: ~${timeLeft} seconds`);
}
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
} catch (e) {
logger.err(`Something went wrong while indexing blocks.` + e);
} }
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
} }
currentBlockHeight -= chunkSize; currentBlockHeight -= chunkSize;
} }
logger.info('Block indexing completed'); logger.info('Block indexing completed');
} catch (e) { } catch (e) {
logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e); logger.err('An error occured in $generateBlockDatabase(). Trying again later. ' + e);
console.log(e); this.blockIndexingStarted = false;
return;
} }
this.blockIndexingCompleted = true; this.blockIndexingCompleted = true;

View File

@ -6,7 +6,7 @@ import logger from '../logger';
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 8; private static currentVersion = 9;
private queryTimeout = 120000; private queryTimeout = 120000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
@ -133,6 +133,17 @@ class DatabaseMigration {
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
} }
if (databaseSchemaVersion < 9 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery(connection, 'ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
}
if (databaseSchemaVersion < 10 && isBitcoin === true) {
await this.$executeQuery(connection, 'ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
}
connection.release(); connection.release();
} catch (e) { } catch (e) {
connection.release(); connection.release();
@ -276,6 +287,10 @@ class DatabaseMigration {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`); queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
} }
if (version < 9) {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`);
}
return queries; return queries;
} }

View File

@ -8,6 +8,7 @@ import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
class Mempool { class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000; private static WEBSOCKET_REFRESH_RATE_MS = 10000;
@ -200,6 +201,17 @@ class Mempool {
logger.debug('Mempool updated in ' + time / 1000 + ' seconds'); logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
} }
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) {
// Store replaced transactions
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
}
}
}
private updateTxPerSecond() { private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD); const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan); this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);

View File

@ -8,6 +8,7 @@ import blocks from './blocks';
class Mining { class Mining {
hashrateIndexingStarted = false; hashrateIndexingStarted = false;
weeklyHashrateIndexingStarted = false;
constructor() { constructor() {
} }
@ -74,140 +75,205 @@ class Mining {
} }
/** /**
* Return the historical difficulty adjustments and oldest indexed block timestamp * [INDEXING] Generate weekly mining pool hashrate history
*/ */
public async $getHistoricalDifficulty(interval: string | null): Promise<object> { public async $generatePoolHashrateHistory(): Promise<void> {
return await BlocksRepository.$getBlocksDifficulty(interval); if (!blocks.blockIndexingCompleted || this.weeklyHashrateIndexingStarted) {
} return;
}
/** // We only run this once a week
* Return the historical hashrates and oldest indexed block timestamp const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_weekly_hashrates_indexing');
*/
public async $getNetworkHistoricalHashrates(interval: string | null): Promise<object> {
return await HashratesRepository.$getNetworkDailyHashrate(interval);
}
/**
* Return the historical hashrates and oldest indexed block timestamp for one or all pools
*/
public async $getPoolsHistoricalHashrates(interval: string | null, poolId: number): Promise<object> {
return await HashratesRepository.$getPoolsWeeklyHashrate(interval);
}
/**
* Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
// We only run this once a day
const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp();
const now = new Date().getTime() / 1000; const now = new Date().getTime() / 1000;
if (now - latestTimestamp < 86400) { if (now - latestTimestamp < 604800) {
return; return;
} }
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) { try {
return; this.weeklyHashrateIndexingStarted = true;
}
this.hashrateIndexingStarted = true;
logger.info(`Indexing hashrates`); logger.info(`Indexing mining pools weekly hashrates`);
const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144; const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); const hashrates: any[] = [];
let startedAt = new Date().getTime() / 1000; const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const lastMidnight = this.getDateMidnight(new Date());
const lastMidnight = new Date(); let toTimestamp = Math.round((lastMidnight.getTime() - 604800) / 1000);
lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0);
let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
let indexedThisRun = 0;
let totalIndexed = 0;
const hashrates: any[] = []; const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008;
let indexedThisRun = 0;
let totalIndexed = 0;
let startedAt = new Date().getTime() / 1000;
while (toTimestamp > genesisTimestamp) { while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400; const fromTimestamp = toTimestamp - 604800;
if (indexedTimestamp.includes(fromTimestamp)) {
toTimestamp -= 86400;
++totalIndexed;
continue;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( // Skip already indexed weeks
null, fromTimestamp, toTimestamp); if (indexedTimestamp.includes(toTimestamp + 1)) {
if (blockStats.blockCount === 0) { // We are done indexing, no blocks left toTimestamp -= 604800;
break; ++totalIndexed;
} continue;
}
let lastBlockHashrate = 0; const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, null, fromTimestamp, toTimestamp);
blockStats.lastBlockHeight); if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
break;
}
if (totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
logger.debug("Indexing weekly hashrates for mining pools"); blockStats.lastBlockHeight);
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp);
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp, toTimestamp);
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
pools = pools.map((pool: any) => { pools = pools.map((pool: any) => {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
pool.share = (pool.blockCount / totalBlocks); pool.share = (pool.blockCount / totalBlocks);
return pool; return pool;
}); });
for (const pool of pools) { for (const pool of pools) {
hashrates.push({ hashrates.push({
hashrateTimestamp: fromTimestamp + 1, hashrateTimestamp: toTimestamp + 1,
avgHashrate: pool['hashrate'], avgHashrate: pool['hashrate'],
poolId: pool.poolId, poolId: pool.poolId,
share: pool['share'], share: pool['share'],
type: 'weekly', type: 'weekly',
}); });
} }
}
hashrates.push({
hashrateTimestamp: fromTimestamp,
avgHashrate: lastBlockHashrate,
poolId: null,
share: 1,
type: 'daily',
});
if (hashrates.length > 10) {
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0; hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2); const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
const daysLeft = Math.round(totalDayIndexed - totalIndexed); const weeksLeft = Math.round(totalWeekIndexed - totalIndexed);
logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds} weeks/sec | ~${weeksLeft} weeks left to index`);
startedAt = new Date().getTime() / 1000; startedAt = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
} }
toTimestamp -= 86400; toTimestamp -= 604800;
++indexedThisRun; ++indexedThisRun;
++totalIndexed; ++totalIndexed;
}
this.weeklyHashrateIndexingStarted = false;
await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing');
logger.info(`Weekly pools hashrate indexing completed`);
} catch (e) {
this.weeklyHashrateIndexingStarted = false;
throw e;
}
}
/**
* [INDEXING] Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
return;
} }
// Add genesis block manually // We only run this once a day
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) { const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp('last_hashrates_indexing');
hashrates.push({ const now = new Date().getTime() / 1000;
hashrateTimestamp: genesisTimestamp, if (now - latestTimestamp < 86400) {
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1), return;
poolId: null,
type: 'daily',
});
} }
if (hashrates.length > 0) { try {
this.hashrateIndexingStarted = true;
logger.info(`Indexing network daily hashrate`);
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMidnight = this.getDateMidnight(new Date());
let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
const hashrates: any[] = [];
const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
let indexedThisRun = 0;
let totalIndexed = 0;
let startedAt = new Date().getTime() / 1000;
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400;
// Skip already indexed weeks
if (indexedTimestamp.includes(fromTimestamp)) {
toTimestamp -= 86400;
++totalIndexed;
continue;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp, toTimestamp);
if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
break;
}
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
hashrates.push({
hashrateTimestamp: toTimestamp,
avgHashrate: lastBlockHashrate,
poolId: null,
share: 1,
type: 'daily',
});
if (hashrates.length > 10) {
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
if (elapsedSeconds > 5) {
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
startedAt = new Date().getTime() / 1000;
indexedThisRun = 0;
}
toTimestamp -= 86400;
++indexedThisRun;
++totalIndexed;
}
// Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
hashrates.push({
hashrateTimestamp: genesisTimestamp,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null,
type: 'daily',
});
}
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
}
await HashratesRepository.$setLatestRunTimestamp();
this.hashrateIndexingStarted = false;
logger.info(`Hashrates indexing completed`); await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing');
this.hashrateIndexingStarted = false;
logger.info(`Daily network hashrate indexing completed`);
} catch (e) {
this.hashrateIndexingStarted = false;
throw e;
}
}
private getDateMidnight(date: Date): Date {
date.setUTCHours(0);
date.setUTCMinutes(0);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
return date;
} }
} }

View File

@ -0,0 +1,34 @@
export interface CachedRbf {
txid: string;
expires: Date;
}
class RbfCache {
private cache: { [txid: string]: CachedRbf; } = {};
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
}
public add(replacedTxId: string, newTxId: string): void {
this.cache[replacedTxId] = {
expires: new Date(Date.now() + 1000 * 604800), // 1 week
txid: newTxId,
};
}
public get(txId: string): CachedRbf | undefined {
return this.cache[txId];
}
private cleanup(): void {
const currentDate = new Date();
for (const c in this.cache) {
if (this.cache[c].expires < currentDate) {
delete this.cache[c];
}
}
}
}
export default new RbfCache();

View File

@ -11,6 +11,7 @@ import { Common } from './common';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import config from '../config'; import config from '../config';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -48,29 +49,38 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-tx']) { if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx']; client['track-tx'] = parsedMessage['track-tx'];
// Client is telling the transaction wasn't found but it might have appeared before we had the time to start watching for it // Client is telling the transaction wasn't found
if (parsedMessage['watch-mempool']) { if (parsedMessage['watch-mempool']) {
const tx = memPool.getMempool()[client['track-tx']]; const rbfCacheTx = rbfCache.get(client['track-tx']);
if (tx) { if (rbfCacheTx) {
if (config.MEMPOOL.BACKEND === 'esplora') { response['txReplaced'] = {
response['tx'] = tx; txid: rbfCacheTx.txid,
};
client['track-tx'] = null;
} else {
// It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[client['track-tx']];
if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx;
} else {
// tx.prevout is missing from transactions when in bitcoind mode
try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
}
}
} else { } else {
// tx.prevouts is missing from transactions when in bitcoind mode
try { try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true); const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
response['tx'] = fullTx; response['tx'] = fullTx;
} catch (e) { } catch (e) {
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e)); logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
client['track-mempool-tx'] = parsedMessage['track-tx'];
} }
} }
} else {
try {
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
client['track-mempool-tx'] = parsedMessage['track-tx'];
}
} }
} }
} else { } else {
@ -221,14 +231,10 @@ class WebsocketHandler {
mempoolBlocks.updateMempoolBlocks(newMempool); mempoolBlocks.updateMempoolBlocks(newMempool);
const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlocks = mempoolBlocks.getMempoolBlocks();
const mempool = memPool.getMempool();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
memPool.handleRbfTransactions(rbfTransactions);
for (const rbfTransaction in rbfTransactions) {
delete mempool[rbfTransaction];
}
this.wss.clients.forEach(async (client: WebSocket) => { this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
@ -331,21 +337,29 @@ class WebsocketHandler {
} }
} }
if (client['track-tx'] && rbfTransactions[client['track-tx']]) { if (client['track-tx']) {
for (const rbfTransaction in rbfTransactions) { const outspends: object = {};
if (client['track-tx'] === rbfTransaction) { newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
const rbfTx = rbfTransactions[rbfTransaction]; if (vin.txid === client['track-tx']) {
if (config.MEMPOOL.BACKEND !== 'esplora') { outspends[vin.vout] = {
try { vin: i,
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true); txid: tx.txid,
response['rbfTransaction'] = fullTx; };
} catch (e) { }
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); }));
}
} else { if (Object.keys(outspends).length) {
response['rbfTransaction'] = rbfTx; response['utxoSpent'] = outspends;
}
if (rbfTransactions[client['track-tx']]) {
for (const rbfTransaction in rbfTransactions) {
if (client['track-tx'] === rbfTransaction) {
response['rbfTransaction'] = {
txid: rbfTransactions[rbfTransaction].txid,
};
break;
} }
break;
} }
} }
} }
@ -405,7 +419,6 @@ class WebsocketHandler {
} }
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) { if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
client['track-tx'] = null;
response['txConfirmed'] = true; response['txConfirmed'] = true;
} }

View File

@ -11,6 +11,7 @@ export class DB {
password: config.DATABASE.PASSWORD, password: config.DATABASE.PASSWORD,
connectionLimit: 10, connectionLimit: 10,
supportBigNumbers: true, supportBigNumbers: true,
timezone: '+00:00',
}); });
} }

View File

@ -96,8 +96,8 @@ class Server {
await Common.sleep(5000); await Common.sleep(5000);
await databaseMigration.$truncateIndexedData(tables); await databaseMigration.$truncateIndexedData(tables);
} }
await this.$resetHashratesIndexingState();
await databaseMigration.$initializeOrMigrateDatabase(); await databaseMigration.$initializeOrMigrateDatabase();
await this.$resetHashratesIndexingState();
await poolsParser.migratePoolsJson(); await poolsParser.migratePoolsJson();
} catch (e) { } catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error'); throw new Error(e instanceof Error ? e.message : 'Error');
@ -167,7 +167,8 @@ class Server {
} }
async $resetHashratesIndexingState() { async $resetHashratesIndexingState() {
return await HashratesRepository.$setLatestRunTimestamp(0); await HashratesRepository.$setLatestRunTimestamp('last_hashrates_indexing', 0);
await HashratesRepository.$setLatestRunTimestamp('last_weekly_hashrates_indexing', 0);
} }
async $runIndexingWhenReady() { async $runIndexingWhenReady() {
@ -176,8 +177,9 @@ class Server {
} }
try { try {
await blocks.$generateBlockDatabase(); blocks.$generateBlockDatabase();
await mining.$generateNetworkHashrateHistory(); mining.$generateNetworkHashrateHistory();
mining.$generatePoolHashrateHistory();
} catch (e) { } catch (e) {
logger.err(`Unable to run indexing right now, trying again later. ` + e); logger.err(`Unable to run indexing right now, trying again later. ` + e);
} }

View File

@ -53,15 +53,17 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
await connection.query(query, params); await connection.query(query, params);
connection.release();
} catch (e: any) { } catch (e: any) {
connection.release();
if (e.errno === 1062) { // ER_DUP_ENTRY if (e.errno === 1062) { // ER_DUP_ENTRY
logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`); logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
} else { } else {
connection.release();
logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e)); logger.err('$saveBlockInDatabase() error' + (e instanceof Error ? e.message : e));
throw e;
} }
} }
connection.release();
} }
/** /**
@ -73,20 +75,26 @@ class BlocksRepository {
} }
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(` try {
SELECT height const [rows]: any[] = await connection.query(`
FROM blocks SELECT height
WHERE height <= ? AND height >= ? FROM blocks
ORDER BY height DESC; WHERE height <= ? AND height >= ?
`, [startHeight, endHeight]); ORDER BY height DESC;
connection.release(); `, [startHeight, endHeight]);
connection.release();
const indexedBlockHeights: number[] = []; const indexedBlockHeights: number[] = [];
rows.forEach((row: any) => { indexedBlockHeights.push(row.height); }); rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse(); const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1); const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
return missingBlocksHeights; return missingBlocksHeights;
} catch (e) {
connection.release();
logger.err('$getMissingBlocksBetweenHeights() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -111,10 +119,16 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, params); try {
connection.release(); const [rows] = await connection.query(query, params);
connection.release();
return <EmptyBlocks[]>rows; return <EmptyBlocks[]>rows;
} catch (e) {
connection.release();
logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -143,10 +157,16 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, params); try {
connection.release(); const [rows] = await connection.query(query, params);
connection.release();
return <number>rows[0].blockCount; return <number>rows[0].blockCount;
} catch (e) {
connection.release();
logger.err('$blockCount() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -177,10 +197,16 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, params); try {
connection.release(); const [rows] = await connection.query(query, params);
connection.release();
return <number>rows[0]; return <number>rows[0];
} catch (e) {
connection.release();
logger.err('$blockCountBetweenTimestamp() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -194,23 +220,26 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(query); try {
connection.release(); const [rows]: any[] = await connection.query(query);
connection.release();
if (rows.length <= 0) { if (rows.length <= 0) {
return -1; return -1;
}
return <number>rows[0].blockTimestamp;
} catch (e) {
connection.release();
logger.err('$oldestBlockTimestamp() error' + (e instanceof Error ? e.message : e));
throw e;
} }
return <number>rows[0].blockTimestamp;
} }
/** /**
* Get blocks mined by a specific mining pool * Get blocks mined by a specific mining pool
*/ */
public async $getBlocksByPool( public async $getBlocksByPool(poolId: number, startHeight: number | null = null): Promise<object[]> {
poolId: number,
startHeight: number | null = null
): Promise<object[]> {
const params: any[] = []; const params: any[] = [];
let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward let query = `SELECT height, hash as id, tx_count, size, weight, pool_id, UNIX_TIMESTAMP(blockTimestamp) as timestamp, reward
FROM blocks FROM blocks
@ -227,14 +256,20 @@ class BlocksRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, params); try {
connection.release(); const [rows] = await connection.query(query, params);
connection.release();
for (const block of <object[]>rows) { for (const block of <object[]>rows) {
delete block['blockTimestamp']; delete block['blockTimestamp'];
}
return <object[]>rows;
} catch (e) {
connection.release();
logger.err('$getBlocksByPool() error' + (e instanceof Error ? e.message : e));
throw e;
} }
return <object[]>rows;
} }
/** /**
@ -242,19 +277,25 @@ class BlocksRepository {
*/ */
public async $getBlockByHeight(height: number): Promise<object | null> { public async $getBlockByHeight(height: number): Promise<object | null> {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(` try {
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 const [rows]: any[] = await connection.query(`
FROM blocks 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
JOIN pools ON blocks.pool_id = pools.id FROM blocks
WHERE height = ${height}; JOIN pools ON blocks.pool_id = pools.id
`); WHERE height = ${height};
connection.release(); `);
connection.release();
if (rows.length <= 0) { if (rows.length <= 0) {
return null; return null;
}
return rows[0];
} catch (e) {
connection.release();
logger.err('$getBlockByHeight() error' + (e instanceof Error ? e.message : e));
throw e;
} }
return rows[0];
} }
/** /**
@ -297,21 +338,34 @@ class BlocksRepository {
ORDER BY t.height ORDER BY t.height
`; `;
const [rows]: any[] = await connection.query(query); try {
connection.release(); const [rows]: any[] = await connection.query(query);
connection.release();
for (let row of rows) { for (let row of rows) {
delete row['rn']; delete row['rn'];
}
return rows;
} catch (e) {
connection.release();
logger.err('$getBlocksDifficulty() error' + (e instanceof Error ? e.message : e));
throw e;
} }
return rows;
} }
public async $getOldestIndexedBlockHeight(): Promise<number> { public async $getOldestIndexedBlockHeight(): Promise<number> {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`); try {
connection.release(); const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
return rows[0].minHeight; connection.release();
return rows[0].minHeight;
} catch (e) {
connection.release();
logger.err('$getOldestIndexedBlockHeight() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
} }

View File

@ -8,6 +8,10 @@ class HashratesRepository {
* Save indexed block data in the database * Save indexed block data in the database
*/ */
public async $saveHashrates(hashrates: any) { public async $saveHashrates(hashrates: any) {
if (hashrates.length === 0) {
return;
}
let query = `INSERT INTO let query = `INSERT INTO
hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`; hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
@ -20,11 +24,12 @@ class HashratesRepository {
try { try {
// logger.debug(query); // logger.debug(query);
await connection.query(query); await connection.query(query);
connection.release();
} catch (e: any) { } catch (e: any) {
connection.release();
logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
throw e;
} }
connection.release();
} }
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> { public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
@ -46,10 +51,33 @@ class HashratesRepository {
query += ` ORDER by hashrate_timestamp`; query += ` ORDER by hashrate_timestamp`;
const [rows]: any[] = await connection.query(query); try {
connection.release(); const [rows]: any[] = await connection.query(query);
connection.release();
return rows; return rows;
} catch (e) {
connection.release();
logger.err('$getNetworkDailyHashrate() error' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
const connection = await DB.pool.getConnection();
const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp FROM hashrates where type = 'weekly' GROUP BY hashrate_timestamp`;
try {
const [rows]: any[] = await connection.query(query);
connection.release();
return rows.map(row => row.timestamp);
} catch (e) {
connection.release();
logger.err('$getWeeklyHashrateTimestamps() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -76,26 +104,44 @@ class HashratesRepository {
query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`; query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
const [rows]: any[] = await connection.query(query); try {
connection.release(); const [rows]: any[] = await connection.query(query);
connection.release();
return rows; return rows;
} catch (e) {
connection.release();
logger.err('$getPoolsWeeklyHashrate() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
public async $setLatestRunTimestamp(val: any = null) { public async $setLatestRunTimestamp(key: string, val: any = null) {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`; const query = `UPDATE state SET number = ? WHERE name = ?`;
await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]); try {
connection.release(); await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000), key] : [val, key]);
connection.release();
} catch (e) {
connection.release();
}
} }
public async $getLatestRunTimestamp(): Promise<number> { public async $getLatestRunTimestamp(key: string): Promise<number> {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`; const query = `SELECT number FROM state WHERE name = ?`;
const [rows] = await connection.query<any>(query);
connection.release(); try {
return rows[0]['number']; const [rows] = await connection.query<any>(query, [key]);
connection.release();
return rows[0]['number'];
} catch (e) {
connection.release();
logger.err('$setLatestRunTimestamp() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
} }

View File

@ -43,26 +43,38 @@ class PoolsRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query); try {
connection.release(); const [rows] = await connection.query(query);
connection.release();
return <PoolInfo[]>rows; return <PoolInfo[]>rows;
} catch (e) {
connection.release();
logger.err('$getPoolsInfo() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
* Get basic pool info and block count between two timestamp * Get basic pool info and block count between two timestamp
*/ */
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> { public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
FROM pools FROM pools
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?) LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
GROUP BY pools.id`; GROUP BY pools.id`;
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, [from, to]); try {
connection.release(); const [rows] = await connection.query(query, [from, to]);
connection.release();
return <PoolInfo[]>rows; return <PoolInfo[]>rows;
} catch (e) {
connection.release();
logger.err('$getPoolsInfoBetween() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
/** /**
@ -76,13 +88,19 @@ class PoolsRepository {
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, [poolId]); try {
connection.release(); const [rows] = await connection.query(query, [poolId]);
connection.release();
rows[0].regexes = JSON.parse(rows[0].regexes); rows[0].regexes = JSON.parse(rows[0].regexes);
rows[0].addresses = JSON.parse(rows[0].addresses); rows[0].addresses = JSON.parse(rows[0].addresses);
return rows[0]; return rows[0];
} catch (e) {
connection.release();
logger.err('$getPool() error' + (e instanceof Error ? e.message : e));
throw e;
}
} }
} }

View File

@ -24,6 +24,7 @@ import miningStats from './api/mining';
import axios from 'axios'; import axios from 'axios';
import mining from './api/mining'; import mining from './api/mining';
import BlocksRepository from './repositories/BlocksRepository'; import BlocksRepository from './repositories/BlocksRepository';
import HashratesRepository from './repositories/HashratesRepository';
class Routes { class Routes {
constructor() {} constructor() {}
@ -576,7 +577,7 @@ class Routes {
public async $getHistoricalDifficulty(req: Request, res: Response) { public async $getHistoricalDifficulty(req: Request, res: Response) {
try { try {
const stats = await mining.$getHistoricalDifficulty(req.params.interval ?? null); const stats = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@ -588,7 +589,7 @@ class Routes {
public async $getPoolsHistoricalHashrate(req: Request, res: Response) { public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try { try {
const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, parseInt(req.params.poolId, 10)); const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null);
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
@ -604,8 +605,8 @@ class Routes {
public async $getHistoricalHashrate(req: Request, res: Response) { public async $getHistoricalHashrate(req: Request, res: Response) {
try { try {
const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null); const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null); const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp(); const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');

3
contributors/bosch-0.txt Normal file
View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: Bosch-0

View File

@ -74,6 +74,7 @@ import { HashrateChartComponent } from './components/hashrate-chart/hashrate-cha
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component'; import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component'; import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -154,6 +155,7 @@ import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
SeoService, SeoService,
StorageService, StorageService,
LanguageService, LanguageService,
ShortenStringPipe,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@ -1,4 +1,4 @@
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div> <div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="card"> <div class="card">
<div class="card-body more-padding"> <div class="card-body more-padding">
@ -47,7 +47,7 @@
</div> </div>
</div> </div>
<div class="item" *ngIf="showHalving"> <div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next halving</h5> <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
<div class="card-text"> <div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container> <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>

View File

@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit {
isLoadingWebSocket$: Observable<boolean>; isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>; difficultyEpoch$: Observable<EpochProgress>;
@Input() showProgress: boolean = true; @Input() showProgress = true;
@Input() showHalving: boolean = false; @Input() showHalving = false;
@Input() showTitle = true;
constructor( constructor(
public stateService: StateService, public stateService: StateService,
@ -97,7 +98,7 @@ export class DifficultyComponent implements OnInit {
colorPreviousAdjustments = '#ffffff66'; colorPreviousAdjustments = '#ffffff66';
} }
const blocksUntilHalving = block.height % 210000; const blocksUntilHalving = 210000 - (block.height % 210000);
const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000); const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000);
return { return {

View File

@ -1,6 +1,6 @@
<div [class]="widget === false ? 'full-container' : ''"> <div [class]="widget === false ? 'full-container' : ''">
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''"> <div *ngIf="!tableOnly" class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
@ -25,34 +25,29 @@
</form> </form>
</div> </div>
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'" <div *ngIf="!tableOnly" [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div> echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<!-- <div class="mt-3" *ngIf="!widget"> <table *ngIf="tableOnly" class="table latest-transactions" style="min-height: 295px">
<table class="table table-borderless table-sm text-center"> <thead>
<thead> <tr>
<tr> <th class="d-none d-md-block" i18n="block.height">Height</th>
<th i18n="mining.rank">Block</th> <th i18n="mining.adjusted" class="text-left">Adjusted</th>
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th> <th i18n="mining.difficulty" class="text-right">Difficulty</th>
<th i18n="mining.adjusted">Adjusted</th> <th i18n="mining.change" class="text-right">Change</th>
<th i18n="mining.difficulty">Difficulty</th> </tr>
<th i18n="mining.change">Change</th> </thead>
</tr> <tbody *ngIf="(hashrateObservable$ | async) as data">
</thead> <tr *ngFor="let diffChange of data.difficulty">
<tbody *ngIf="(hashrateObservable$ | async) as data"> <td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<tr *ngFor="let diffChange of data.difficulty"> <td class="text-left"><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td> <td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="d-none d-md-block">&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td> <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td> </tr>
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td> </tbody>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td> </table>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div> -->
</div> </div>

View File

@ -29,7 +29,7 @@
.chart-widget { .chart-widget {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 275px; max-height: 293px;
} }
.formRadioGroup { .formRadioGroup {
@ -48,3 +48,44 @@
} }
} }
} }
.latest-transactions {
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
border: 0px;
}
td {
width: 25%;
}
.table-cell-satoshis {
display: none;
text-align: right;
@media (min-width: 576px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 1100px) {
display: table-cell;
}
}
.table-cell-fiat {
display: none;
text-align: right;
@media (min-width: 485px) {
display: table-cell;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: table-cell;
}
}
.table-cell-fees {
text-align: right;
}
}

View File

@ -15,14 +15,15 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils';
styles: [` styles: [`
.loadingGraphs { .loadingGraphs {
position: absolute; position: absolute;
top: 38%; top: 50%;
left: calc(50% - 15px); left: calc(50% - 15px);
z-index: 100; z-index: 100;
} }
`], `],
}) })
export class HashrateChartComponent implements OnInit { export class HashrateChartComponent implements OnInit {
@Input() widget: boolean = false; @Input() tableOnly = false;
@Input() widget = false;
@Input() right: number | string = 45; @Input() right: number | string = 45;
@Input() left: number | string = 75; @Input() left: number | string = 75;
@ -114,7 +115,7 @@ export class HashrateChartComponent implements OnInit {
} }
return { return {
availableTimespanDay: availableTimespanDay, availableTimespanDay: availableTimespanDay,
difficulty: tableData difficulty: this.tableOnly ? tableData.slice(0, 5) : tableData
}; };
}), }),
); );
@ -141,6 +142,7 @@ export class HashrateChartComponent implements OnInit {
bottom: this.widget ? 30 : 60, bottom: this.widget ? 30 : 60,
}, },
tooltip: { tooltip: {
show: !this.isMobile() || !this.widget,
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'line' type: 'line'

View File

@ -25,9 +25,9 @@
</form> </form>
</div> </div>
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'" <div [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div> echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>

View File

@ -29,7 +29,7 @@
.chart-widget { .chart-widget {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 275px; max-height: 293px;
} }
.formRadioGroup { .formRadioGroup {

View File

@ -78,7 +78,7 @@ export class HashrateChartPoolsComponent implements OnInit {
name: name, name: name,
showSymbol: false, showSymbol: false,
symbol: 'none', symbol: 'none',
data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]), data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]),
type: 'line', type: 'line',
lineStyle: { width: 0 }, lineStyle: { width: 0 },
areaStyle: { opacity: 1 }, areaStyle: { opacity: 1 },
@ -132,6 +132,7 @@ export class HashrateChartPoolsComponent implements OnInit {
top: this.widget ? 10 : 40, top: this.widget ? 10 : 40,
}, },
tooltip: { tooltip: {
show: !this.isMobile() || !this.widget,
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'line' type: 'line'
@ -149,7 +150,7 @@ export class HashrateChartPoolsComponent implements OnInit {
data.sort((a, b) => b.data[1] - a.data[1]); data.sort((a, b) => b.data[1] - a.data[1]);
for (const pool of data) { for (const pool of data) {
if (pool.data[1] > 0) { if (pool.data[1] > 0) {
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%<br>` tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1].toFixed(2)}%<br>`;
} }
} }
return tooltip; return tooltip;

View File

@ -2,36 +2,124 @@
<div class="row row-cols-1 row-cols-md-2"> <div class="row row-cols-1 row-cols-md-2">
<!-- Temporary stuff here - Will be moved to a component once we have more useful data to show -->
<div class="col"> <div class="col">
<div class="card double"> <div class="main-title">Reward stats</div>
<div class="card-body"> <div class="card" style="height: 123px">
<!-- pool distribution --> <div class="card-body more-padding">
<h5 class="card-title"> <div class="fee-estimation-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward">
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share"> <div class="item">
Mining Pools Share (1w) <h5 class="card-title" i18n="">Miners Reward</h5>
</a> <div class="card-text">
</h5> <app-amount [satoshis]="rewardStats.totalReward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<app-pool-ranking [widget]=true></app-pool-ranking> <div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Reward Per Tx</h5>
<div class="card-text">
{{ rewardStats.rewardPerTx | amountShortener }}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Average Fee</h5>
<div class="card-text">
{{ rewardStats.feePerTx | amountShortener}}
<span class="symbol">sats/tx</span>
<div class="symbol">in the last 8 blocks</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #loadingReward>
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="">Miners Reward</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Reward Per Tx</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="">Average Fee</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
</div>
</ng-template>
<!-- pools hashrate --> <!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card" style="height: 123px">
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
</div>
<!-- pool distribution -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<app-pool-ranking [widget]=true></app-pool-ranking>
<div class="mt-1"><a [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
<!-- hashrate -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<h5 class="card-title">
Hashrate (1y)
</h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div>
</div>
</div>
<!-- pool dominance -->
<div class="col">
<div class="card" style="height: 385px">
<div class="card-body">
<h5 class="card-title">
Mining Pools Dominance (1y)
</h5>
<app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools> <app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
<div class="mt-1"><a [routerLink]="['/mining/hashrate/pools' | relativeUrl]" i18n="dashboard.view-more">View
more &raquo;</a></div>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="card"> <div class="card" style="height: 385px">
<div class="card-body"> <div class="card-body">
<!-- hashrate -->
<h5 class="card-title"> <h5 class="card-title">
<a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate"> Adjustments
Hashrate (1y)
</a>
</h5> </h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart> <app-hashrate-chart [tableOnly]=true [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,14 +12,11 @@
.card { .card {
background-color: #1d1f31; background-color: #1d1f31;
height: 340px;
}
.card.double {
height: 620px;
} }
.card-title { .card-title {
font-size: 1rem; font-size: 1rem;
color: #4a68b9;
} }
.card-title > a { .card-title > a {
color: #4a68b9; color: #4a68b9;
@ -47,7 +44,7 @@
.fade-border { .fade-border {
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%) -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
} }
.main-title { .main-title {
position: relative; position: relative;
color: #ffffff91; color: #ffffff91;
@ -58,3 +55,63 @@
text-align: center; text-align: center;
padding-bottom: 3px; padding-bottom: 3px;
} }
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
&:last-child {
margin-bottom: 0;
}
.card-text span {
color: #ffffff66;
font-size: 12px;
top: 0px;
}
.fee-text{
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
}
.fiat {
display: block;
font-size: 14px !important;
}
}
}
.skeleton-loader {
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}

View File

@ -1,5 +1,8 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { map } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-mining-dashboard', selector: 'app-mining-dashboard',
@ -8,12 +11,35 @@ import { SeoService } from 'src/app/services/seo.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class MiningDashboardComponent implements OnInit { export class MiningDashboardComponent implements OnInit {
private blocks = [];
constructor(private seoService: SeoService) { public $rewardStats: Observable<any>;
public totalReward = 0;
public rewardPerTx = '~';
public feePerTx = '~';
constructor(private seoService: SeoService,
public stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`); this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
} }
ngOnInit(): void { ngOnInit(): void {
} this.$rewardStats = this.stateService.blocks$.pipe(
map(([block]) => {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 8);
const totalTx = this.blocks.reduce((acc, block) => acc + block.tx_count, 0);
const totalFee = this.blocks.reduce((acc, block) => acc + block.extras?.totalFees ?? 0, 0);
const totalReward = this.blocks.reduce((acc, block) => acc + block.extras?.reward ?? 0, 0);
return {
'totalReward': totalReward,
'rewardPerTx': totalReward / totalTx,
'feePerTx': totalFee / totalTx,
}
})
);
}
} }

View File

@ -1,8 +1,31 @@
<div [class]="widget === false ? 'container-xl' : ''"> <div [class]="widget === false ? 'container-xl' : ''">
<div *ngIf="widget">
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<p class="card-text">
{{ miningStats['minersLuck'] }}%
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
<p class="card-text">
{{ miningStats.blockCount }}
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<p class="card-text">
{{ miningStats.pools.length }}
</p>
</div>
</div>
</div>
<div [class]="widget ? 'chart-widget' : 'chart'" <div [class]="widget ? 'chart-widget' : 'chart'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div> echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
@ -59,7 +82,7 @@
<td class="d-none d-md-block">{{ pool.rank }}</td> <td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td> <td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td>
<td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td> <td class=""><a [routerLink]="[('/mining/pool/' + pool.poolId) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.poolsWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td> <td class="" *ngIf="this.poolsWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td class="">{{ pool['blockText'] }}</td> <td class="">{{ pool['blockText'] }}</td>
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td> <td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr> </tr>
@ -75,3 +98,27 @@
</table> </table>
</div> </div>
<ng-template #loadingReward>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Blocks (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
</div>
</ng-template>

View File

@ -1,13 +1,16 @@
.chart { .chart {
max-height: 400px; max-height: 400px;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
max-height: 300px; max-height: 270px;
} }
} }
.chart-widget { .chart-widget {
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 275px; max-height: 270px;
@media (max-width: 767.98px) {
max-height: 200px;
}
} }
.formRadioGroup { .formRadioGroup {
@ -44,3 +47,66 @@
.loadingGraphs.widget { .loadingGraphs.widget {
top: 25%; top: 25%;
} }
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
margin: 0px auto 10px;
display: inline-block;
@media (min-width: 485px) {
margin: 0px auto 10px;
}
@media (min-width: 785px) {
margin: 0px auto 0px;
}
&:last-child {
margin: 0px auto 0px;
}
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts'; import { EChartsOption, PieSeriesOption } from 'echarts';
@ -41,6 +41,7 @@ export class PoolRankingComponent implements OnInit {
private miningService: MiningService, private miningService: MiningService,
private seoService: SeoService, private seoService: SeoService,
private router: Router, private router: Router,
private zone: NgZone,
) { ) {
} }
@ -49,7 +50,7 @@ export class PoolRankingComponent implements OnInit {
this.poolsWindowPreference = '1w'; this.poolsWindowPreference = '1w';
} else { } else {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
} }
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference); this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
@ -85,6 +86,7 @@ export class PoolRankingComponent implements OnInit {
}), }),
map(data => { map(data => {
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool)); data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w
return data; return data;
}), }),
tap(data => { tap(data => {
@ -105,24 +107,40 @@ export class PoolRankingComponent implements OnInit {
} }
generatePoolsChartSerieData(miningStats) { generatePoolsChartSerieData(miningStats) {
const poolShareThreshold = this.isMobile() ? 1 : 0.5; // Do not draw pools which hashrate share is lower than that const poolShareThreshold = this.isMobile() ? 2 : 1; // Do not draw pools which hashrate share is lower than that
const data: object[] = []; const data: object[] = [];
let totalShareOther = 0;
let totalBlockOther = 0;
let totalEstimatedHashrateOther = 0;
let edgeDistance: any = '20%';
if (this.isMobile() && this.widget) {
edgeDistance = 0;
} else if (this.isMobile() && !this.widget || this.widget) {
edgeDistance = 35;
}
miningStats.pools.forEach((pool) => { miningStats.pools.forEach((pool) => {
if (parseFloat(pool.share) < poolShareThreshold) { if (parseFloat(pool.share) < poolShareThreshold) {
totalShareOther += parseFloat(pool.share);
totalBlockOther += pool.blockCount;
totalEstimatedHashrateOther += pool.lastEstimatedHashrate;
return; return;
} }
data.push({ data.push({
itemStyle: { itemStyle: {
color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
}, },
value: pool.share, value: pool.share,
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), name: pool.name + ((this.isMobile() || this.widget) ? `` : ` (${pool.share}%)`),
label: { label: {
overflow: 'none',
color: '#b1b1b1', color: '#b1b1b1',
overflow: 'break', alignTo: 'edge',
edgeDistance: edgeDistance,
}, },
tooltip: { tooltip: {
show: !this.isMobile() || !this.widget,
backgroundColor: 'rgba(17, 19, 31, 1)', backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4, borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)', shadowColor: 'rgba(0, 0, 0, 0.5)',
@ -144,6 +162,42 @@ export class PoolRankingComponent implements OnInit {
data: pool.poolId, data: pool.poolId,
} as PieSeriesOption); } as PieSeriesOption);
}); });
// 'Other'
data.push({
itemStyle: {
color: 'grey',
},
value: totalShareOther,
name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
label: {
overflow: 'none',
color: '#b1b1b1',
alignTo: 'edge',
edgeDistance: edgeDistance
},
tooltip: {
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: () => {
if (this.poolsWindowPreference === '24h') {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' +
`<br>` + totalBlockOther.toString() + ` blocks`;
} else {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalBlockOther.toString() + ` blocks`;
}
}
},
data: 9999 as any,
} as PieSeriesOption);
return data; return data;
} }
@ -154,9 +208,22 @@ export class PoolRankingComponent implements OnInit {
} }
network = network.charAt(0).toUpperCase() + network.slice(1); network = network.charAt(0).toUpperCase() + network.slice(1);
let radius: any[] = ['20%', '70%']; let radius: any[] = ['20%', '80%'];
if (this.isMobile() || this.widget) { let top: any = undefined; let bottom = undefined; let height = undefined;
radius = ['20%', '60%']; if (this.isMobile() && this.widget) {
top = -30;
height = 270;
radius = ['10%', '50%'];
} else if (this.isMobile() && !this.widget) {
top = 0;
height = 300;
radius = ['10%', '50%'];
} else if (this.widget) {
radius = ['15%', '60%'];
top = -20;
height = 330;
} else {
top = 35;
} }
this.chartOptions = { this.chartOptions = {
@ -180,14 +247,15 @@ export class PoolRankingComponent implements OnInit {
}, },
series: [ series: [
{ {
top: this.widget ? 0 : 35, minShowLabelAngle: 3.6,
top: top,
bottom: bottom,
height: height,
name: 'Mining pool', name: 'Mining pool',
type: 'pie', type: 'pie',
radius: radius, radius: radius,
data: this.generatePoolsChartSerieData(miningStats), data: this.generatePoolsChartSerieData(miningStats),
labelLine: { labelLine: {
length: this.isMobile() ? 10 : 15,
length2: this.isMobile() ? 0 : 15,
lineStyle: { lineStyle: {
width: 2, width: 2,
}, },
@ -223,14 +291,19 @@ export class PoolRankingComponent implements OnInit {
this.chartInstance = ec; this.chartInstance = ec;
this.chartInstance.on('click', (e) => { this.chartInstance.on('click', (e) => {
this.router.navigate(['/mining/pool/', e.data.data]); if (e.data.data === 9999) { // "Other"
return;
}
this.zone.run(() => {
this.router.navigate(['/mining/pool/', e.data.data]);
});
}); });
} }
/** /**
* Default mining stats if something goes wrong * Default mining stats if something goes wrong
*/ */
getEmptyMiningStat() { getEmptyMiningStat(): MiningStats {
return { return {
lastEstimatedHashrate: 'Error', lastEstimatedHashrate: 'Error',
blockCount: 0, blockCount: 0,

View File

@ -1,7 +1,7 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate> <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex"> <div class="d-flex">
<div class="search-box-container mr-2"> <div class="search-box-container mr-2">
<input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="TXID, block height, hash or address"> <input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" [resultFormatter]="formatterFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="TXID, block height, hash or address">
</div> </div>
<div> <div>
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"><fa-icon [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon></button> <button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary"><fa-icon [icon]="['fas', 'search']" [fixedWidth]="true" i18n-title="search-form.search-title" title="Search"></fa-icon></button>

View File

@ -1,8 +1,19 @@
:host ::ng-deep .dropdown-item { :host ::ng-deep {
white-space: nowrap; .dropdown-item {
overflow: hidden; white-space: nowrap;
width: 375px; width: calc(100% - 34px);
text-overflow: ellipsis; }
.dropdown-menu {
width: calc(100% - 34px);
}
@media (min-width: 768px) {
.dropdown-item {
width: 410px;
}
.dropdown-menu {
width: 410px;
}
}
} }
form { form {

View File

@ -8,6 +8,7 @@ import { debounceTime, distinctUntilChanged, switchMap, filter, catchError, map
import { ElectrsApiService } from 'src/app/services/electrs-api.service'; import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { ShortenStringPipe } from 'src/app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
@Component({ @Component({
selector: 'app-search-form', selector: 'app-search-form',
@ -22,6 +23,7 @@ export class SearchFormComponent implements OnInit {
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>); typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
searchForm: FormGroup; searchForm: FormGroup;
isMobile = (window.innerWidth <= 767.98);
@Output() searchTriggered = new EventEmitter(); @Output() searchTriggered = new EventEmitter();
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/; regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
@ -33,6 +35,8 @@ export class SearchFormComponent implements OnInit {
focus$ = new Subject<string>(); focus$ = new Subject<string>();
click$ = new Subject<string>(); click$ = new Subject<string>();
formatterFn = (address: string) => this.shortenStringPipe.transform(address, this.isMobile ? 33 : 40);
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private router: Router, private router: Router,
@ -40,6 +44,7 @@ export class SearchFormComponent implements OnInit {
private stateService: StateService, private stateService: StateService,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private shortenStringPipe: ShortenStringPipe,
) { } ) { }
ngOnInit() { ngOnInit() {

View File

@ -3,13 +3,13 @@
<div class="title-block"> <div class="title-block">
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert"> <div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span> <span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction }"> <a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span> <span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span> <span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
</a> </a>
</div> </div>
<ng-container> <ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
<h1 i18n="shared.transaction">Transaction</h1> <h1 i18n="shared.transaction">Transaction</h1>
<span class="tx-link float-left"> <span class="tx-link float-left">

View File

@ -37,6 +37,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
transactionTime = -1; transactionTime = -1;
subscription: Subscription; subscription: Subscription;
fetchCpfpSubscription: Subscription; fetchCpfpSubscription: Subscription;
txReplacedSubscription: Subscription;
blocksSubscription: Subscription;
rbfTransaction: undefined | Transaction; rbfTransaction: undefined | Transaction;
cpfpInfo: CpfpInfo | null; cpfpInfo: CpfpInfo | null;
showCpfpDetails = false; showCpfpDetails = false;
@ -185,15 +187,12 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.error = undefined; this.error = undefined;
this.waitingForTransaction = false; this.waitingForTransaction = false;
this.setMempoolBlocksSubscription(); this.setMempoolBlocksSubscription();
this.websocketService.startTrackTransaction(tx.txid);
if (!tx.status.confirmed) { if (!tx.status.confirmed && tx.firstSeen) {
this.websocketService.startTrackTransaction(tx.txid); this.transactionTime = tx.firstSeen;
} else {
if (tx.firstSeen) { this.getTransactionTime();
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
}
} }
if (this.tx.status.confirmed) { if (this.tx.status.confirmed) {
@ -220,7 +219,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
} }
); );
this.stateService.blocks$.subscribe(([block, txConfirmed]) => { this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block; this.latestBlock = block;
if (txConfirmed && this.tx) { if (txConfirmed && this.tx) {
@ -235,9 +234,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
} }
}); });
this.stateService.txReplaced$.subscribe( this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
(rbfTransaction) => (this.rbfTransaction = rbfTransaction) if (!this.tx) {
); this.error = new Error();
this.waitingForTransaction = false;
}
this.rbfTransaction = rbfTransaction;
});
} }
handleLoadElectrsTransactionError(error: any): Observable<any> { handleLoadElectrsTransactionError(error: any): Observable<any> {
@ -305,6 +308,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.fetchCpfpSubscription.unsubscribe(); this.fetchCpfpSubscription.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.leaveTransaction(); this.leaveTransaction();
} }
} }

View File

@ -73,16 +73,19 @@
</td> </td>
<td class="text-right nowrap amount"> <td class="text-right nowrap amount">
<ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> <ng-template [ngIf]="vin.prevout && vin.prevout.asset && vin.prevout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset]"> <div *ngIf="assetsMinimal && assetsMinimal[vin.prevout.asset] else assetVinNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vin.prevout }"></ng-container> <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vin.prevout }"></ng-container>
</div> </div>
<ng-template #assetVinNotFound>
{{ vin.prevout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vin.prevout.asset]">{{ vin.prevout.asset | slice : 0 : 7 }}</a>
</ng-template>
</ng-template> </ng-template>
<ng-template #defaultOutput> <ng-template #defaultOutput>
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount> <app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
</ng-template> </ng-template>
</td> </td>
</tr> </tr>
<tr *ngIf="displayDetails"> <tr *ngIf="(showDetails$ | async) === true">
<td colspan="3" class="details-container" > <td colspan="3" class="details-container" >
<table class="table table-striped table-borderless details-table mb-3"> <table class="table table-striped table-borderless details-table mb-3">
<tbody> <tbody>
@ -204,7 +207,7 @@
</ng-template> </ng-template>
</td> </td>
</tr> </tr>
<tr *ngIf="displayDetails"> <tr *ngIf="(showDetails$ | async) === true">
<td colspan="3" class=" details-container" > <td colspan="3" class=" details-container" >
<table class="table table-striped table-borderless details-table mb-3"> <table class="table table-striped table-borderless details-table mb-3">
<tbody> <tbody>
@ -240,7 +243,7 @@
</div> </div>
<div> <div>
<div class="float-left mt-2-5" *ngIf="!transactionPage && tx.fee"> <div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase">
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block">&nbsp;&ndash; {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span> {{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block">&nbsp;&ndash; {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
</div> </div>

View File

@ -1,11 +1,11 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Observable, forkJoin } from 'rxjs'; import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge, of, Subject, Subscription } from 'rxjs';
import { Outspend, Transaction } from '../../interfaces/electrs.interface'; import { Outspend, Transaction } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { AssetsService } from 'src/app/services/assets.service'; import { AssetsService } from 'src/app/services/assets.service';
import { map } from 'rxjs/operators'; import { map, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended } from 'src/app/interfaces/node-api.interface'; import { BlockExtended } from 'src/app/interfaces/node-api.interface';
@Component({ @Component({
@ -17,7 +17,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
export class TransactionsListComponent implements OnInit, OnChanges { export class TransactionsListComponent implements OnInit, OnChanges {
network = ''; network = '';
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
displayDetails = false;
@Input() transactions: Transaction[]; @Input() transactions: Transaction[];
@Input() showConfirmations = false; @Input() showConfirmations = false;
@ -28,7 +27,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Output() loadMore = new EventEmitter(); @Output() loadMore = new EventEmitter();
latestBlock$: Observable<BlockExtended>; latestBlock$: Observable<BlockExtended>;
outspends: Outspend[] = []; outspendsSubscription: Subscription;
refreshOutspends$: ReplaySubject<object> = new ReplaySubject();
showDetails$ = new BehaviorSubject<boolean>(false);
outspends: Outspend[][] = [];
assetsMinimal: any; assetsMinimal: any;
constructor( constructor(
@ -47,6 +49,34 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.assetsMinimal = assets; this.assetsMinimal = assets;
}); });
} }
this.outspendsSubscription = merge(
this.refreshOutspends$
.pipe(
switchMap((observableObject) => forkJoin(observableObject)),
map((outspends: any) => {
const newOutspends: Outspend[] = [];
for (const i in outspends) {
if (outspends.hasOwnProperty(i)) {
newOutspends.push(outspends[i]);
}
}
this.outspends = this.outspends.concat(newOutspends);
}),
),
this.stateService.utxoSpent$
.pipe(
map((utxoSpent) => {
for (const i in utxoSpent) {
this.outspends[0][i] = {
spent: true,
txid: utxoSpent[i].txid,
vin: utxoSpent[i].vin,
};
}
}),
)
).subscribe(() => this.ref.markForCheck());
} }
ngOnChanges() { ngOnChanges() {
@ -70,18 +100,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
observableObject[i] = this.electrsApiService.getOutspends$(tx.txid); observableObject[i] = this.electrsApiService.getOutspends$(tx.txid);
}); });
this.refreshOutspends$.next(observableObject);
forkJoin(observableObject)
.subscribe((outspends: any) => {
const newOutspends = [];
for (const i in outspends) {
if (outspends.hasOwnProperty(i)) {
newOutspends.push(outspends[i]);
}
}
this.outspends = this.outspends.concat(newOutspends);
this.ref.markForCheck();
});
} }
onScroll() { onScroll() {
@ -129,7 +148,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
toggleDetails() { toggleDetails() {
this.displayDetails = !this.displayDetails; if (this.showDetails$.value === true) {
this.ref.markForCheck(); this.showDetails$.next(false);
} else {
this.showDetails$.next(true);
}
}
ngOnDestroy() {
this.outspendsSubscription.unsubscribe();
} }
} }

View File

@ -64,7 +64,7 @@ export interface SinglePoolStats {
blockCount: number; blockCount: number;
emptyBlocks: number; emptyBlocks: number;
rank: number; rank: number;
share: string; share: number;
lastEstimatedHashrate: string; lastEstimatedHashrate: string;
emptyBlockRatio: string; emptyBlockRatio: string;
logo: string; logo: string;
@ -75,13 +75,6 @@ export interface PoolsStats {
oldestIndexedBlockTimestamp: number; oldestIndexedBlockTimestamp: number;
pools: SinglePoolStats[]; pools: SinglePoolStats[];
} }
export interface MiningStats {
lastEstimatedHashrate: string;
blockCount: number;
totalEmptyBlock: number;
totalEmptyBlockRatio: string;
pools: SinglePoolStats[];
}
/** /**
* Pool component * Pool component

View File

@ -15,7 +15,9 @@ export interface WebsocketResponse {
action?: string; action?: string;
data?: string[]; data?: string[];
tx?: Transaction; tx?: Transaction;
rbfTransaction?: Transaction; rbfTransaction?: ReplacedTransaction;
txReplaced?: ReplacedTransaction;
utxoSpent?: object;
transactions?: TransactionStripped[]; transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators; loadingIndicators?: ILoadingIndicators;
backendInfo?: IBackendInfo; backendInfo?: IBackendInfo;
@ -26,6 +28,9 @@ export interface WebsocketResponse {
'track-bisq-market'?: string; 'track-bisq-market'?: string;
} }
export interface ReplacedTransaction extends Transaction {
txid: string;
}
export interface MempoolBlock { export interface MempoolBlock {
blink?: boolean; blink?: boolean;
height?: number; height?: number;

View File

@ -73,7 +73,7 @@ export class MiningService {
const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2); const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2);
const poolsStats = stats.pools.map((poolStat) => { const poolsStats = stats.pools.map((poolStat) => {
return { return {
share: (poolStat.blockCount / stats.blockCount * 100).toFixed(2), share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2), lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2), emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg', logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
@ -82,7 +82,8 @@ export class StateService {
bsqPrice$ = new ReplaySubject<number>(1); bsqPrice$ = new ReplaySubject<number>(1);
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1); mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
txReplaced$ = new Subject<Transaction>(); txReplaced$ = new Subject<ReplacedTransaction>();
utxoSpent$ = new Subject<object>();
mempoolTransactions$ = new Subject<Transaction>(); mempoolTransactions$ = new Subject<Transaction>();
blockTransactions$ = new Subject<Transaction>(); blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1); isLoadingWebSocket$ = new ReplaySubject<boolean>(1);

View File

@ -239,6 +239,10 @@ export class WebsocketService {
this.stateService.txReplaced$.next(response.rbfTransaction); this.stateService.txReplaced$.next(response.rbfTransaction);
} }
if (response.txReplaced) {
this.stateService.txReplaced$.next(response.txReplaced);
}
if (response['mempool-blocks']) { if (response['mempool-blocks']) {
this.stateService.mempoolBlocks$.next(response['mempool-blocks']); this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
} }
@ -251,6 +255,10 @@ export class WebsocketService {
this.stateService.bsqPrice$.next(response['bsq-price']); this.stateService.bsqPrice$.next(response['bsq-price']);
} }
if (response.utxoSpent) {
this.stateService.utxoSpent$.next(response.utxoSpent);
}
if (response.backendInfo) { if (response.backendInfo) {
this.stateService.backendInfo$.next(response.backendInfo); this.stateService.backendInfo$.next(response.backendInfo);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 19 KiB