Merge branch 'master' into wiz/installer2
This commit is contained in:
commit
6fc0311b8e
@ -41,7 +41,9 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
return this.bitcoindClient.getChainTips()
|
||||
.then((result: IBitcoinApi.ChainTips[]) => result[0].height);
|
||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
||||
return result.find(tip => tip.status === 'active')!.height;
|
||||
});
|
||||
}
|
||||
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||
@ -216,7 +218,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
if (map[outputType]) {
|
||||
return map[outputType];
|
||||
} else {
|
||||
return '';
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,17 +108,14 @@ class Blocks {
|
||||
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
|
||||
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
||||
|
||||
const transactionsTmp = [...transactions];
|
||||
transactionsTmp.shift();
|
||||
transactionsTmp.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
|
||||
|
||||
blockExtended.extras.medianFee = transactionsTmp.length > 0 ?
|
||||
Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
||||
blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
|
||||
Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
|
||||
blockExtended.extras.totalFees = transactionsTmp.reduce((acc, tx) => {
|
||||
return acc + tx.fee;
|
||||
}, 0)
|
||||
const stats = await bitcoinClient.getBlockStats(block.id);
|
||||
const coinbaseRaw: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true);
|
||||
blockExtended.extras.coinbaseRaw = coinbaseRaw.hex;
|
||||
blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
|
||||
blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat();
|
||||
blockExtended.extras.totalFees = stats.totalfee;
|
||||
blockExtended.extras.avgFee = stats.avgfee;
|
||||
blockExtended.extras.avgFeeRate = stats.avgfeerate;
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
let pool: PoolTag;
|
||||
@ -184,7 +181,6 @@ class Blocks {
|
||||
}
|
||||
|
||||
this.blockIndexingStarted = true;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
|
||||
try {
|
||||
let currentBlockHeight = blockchainInfo.blocks;
|
||||
@ -201,6 +197,9 @@ class Blocks {
|
||||
const chunkSize = 10000;
|
||||
let totaIndexed = await blocksRepository.$blockCount(null, null);
|
||||
let indexedThisRun = 0;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
while (currentBlockHeight >= lastBlockToIndex) {
|
||||
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
||||
|
||||
@ -219,12 +218,16 @@ class Blocks {
|
||||
break;
|
||||
}
|
||||
++indexedThisRun;
|
||||
if (++totaIndexed % 100 === 0 || blockHeight === lastBlockToIndex) {
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
++totaIndexed;
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||
const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
|
||||
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: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||
const block = await bitcoinApi.$getBlock(blockHash);
|
||||
@ -249,7 +252,7 @@ class Blocks {
|
||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||
|
||||
if (this.blocks.length === 0) {
|
||||
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
|
||||
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
|
||||
} else {
|
||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||
}
|
||||
@ -268,17 +271,19 @@ class Blocks {
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
this.currentDifficulty = block.difficulty;
|
||||
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
if (blockHeightTip >= 2016) {
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Blockchain headers (${blockchainInfo.headers}) and blocks (${blockchainInfo.blocks}) not in sync. Waiting...`);
|
||||
}
|
||||
}
|
||||
|
||||
while (this.currentBlockHeight < blockHeightTip) {
|
||||
if (this.currentBlockHeight === 0) {
|
||||
if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) {
|
||||
this.currentBlockHeight = blockHeightTip;
|
||||
} else {
|
||||
this.currentBlockHeight++;
|
||||
|
@ -6,7 +6,7 @@ import logger from '../logger';
|
||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 9;
|
||||
private static currentVersion = 11;
|
||||
private queryTimeout = 120000;
|
||||
private statisticsAddedIndexed = false;
|
||||
|
||||
@ -92,13 +92,13 @@ class DatabaseMigration {
|
||||
await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
||||
}
|
||||
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
|
||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
||||
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
||||
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
|
||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
||||
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
||||
// Cleanup original blocks fields type
|
||||
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
||||
@ -125,7 +125,7 @@ class DatabaseMigration {
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
|
||||
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 `hashrates` DROP INDEX `PRIMARY`');
|
||||
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||
@ -134,12 +134,28 @@ class DatabaseMigration {
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 9 && isBitcoin === true) {
|
||||
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
|
||||
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`)');
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 11 && isBitcoin === true) {
|
||||
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
|
||||
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
||||
await this.$executeQuery(connection, `ALTER TABLE blocks
|
||||
ADD avg_fee INT UNSIGNED NULL,
|
||||
ADD avg_fee_rate INT UNSIGNED NULL
|
||||
`);
|
||||
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
|
||||
}
|
||||
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
connection.release();
|
||||
|
@ -8,6 +8,7 @@ import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
||||
import loadingIndicators from './loading-indicators';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class Mempool {
|
||||
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
|
||||
@ -200,6 +201,17 @@ class Mempool {
|
||||
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() {
|
||||
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
|
||||
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PoolInfo, PoolStats } from '../mempool.interfaces';
|
||||
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
|
||||
import BlocksRepository from '../repositories/BlocksRepository';
|
||||
import PoolsRepository from '../repositories/PoolsRepository';
|
||||
import HashratesRepository from '../repositories/HashratesRepository';
|
||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
@ -20,25 +20,21 @@ class Mining {
|
||||
const poolsStatistics = {};
|
||||
|
||||
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
|
||||
const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(null, interval);
|
||||
const emptyBlocks: any[] = await BlocksRepository.$countEmptyBlocks(null, interval);
|
||||
|
||||
const poolsStats: PoolStats[] = [];
|
||||
let rank = 1;
|
||||
|
||||
poolsInfo.forEach((poolInfo: PoolInfo) => {
|
||||
const emptyBlocksCount = emptyBlocks.filter((emptyCount) => emptyCount.poolId === poolInfo.poolId);
|
||||
const poolStat: PoolStats = {
|
||||
poolId: poolInfo.poolId, // mysql row id
|
||||
name: poolInfo.name,
|
||||
link: poolInfo.link,
|
||||
blockCount: poolInfo.blockCount,
|
||||
rank: rank++,
|
||||
emptyBlocks: 0
|
||||
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0
|
||||
};
|
||||
for (let i = 0; i < emptyBlocks.length; ++i) {
|
||||
if (emptyBlocks[i].poolId === poolInfo.poolId) {
|
||||
poolStat.emptyBlocks++;
|
||||
}
|
||||
}
|
||||
poolsStats.push(poolStat);
|
||||
});
|
||||
|
||||
@ -58,19 +54,19 @@ class Mining {
|
||||
/**
|
||||
* Get all mining pool stats for a pool
|
||||
*/
|
||||
public async $getPoolStat(interval: string | null, poolId: number): Promise<object> {
|
||||
public async $getPoolStat(poolId: number): Promise<object> {
|
||||
const pool = await PoolsRepository.$getPool(poolId);
|
||||
if (!pool) {
|
||||
throw new Error(`This mining pool does not exist`);
|
||||
}
|
||||
|
||||
const blockCount: number = await BlocksRepository.$blockCount(poolId, interval);
|
||||
const emptyBlocks: EmptyBlocks[] = await BlocksRepository.$getEmptyBlocks(poolId, interval);
|
||||
const blockCount: number = await BlocksRepository.$blockCount(poolId);
|
||||
const emptyBlocksCount = await BlocksRepository.$countEmptyBlocks(poolId);
|
||||
|
||||
return {
|
||||
pool: pool,
|
||||
blockCount: blockCount,
|
||||
emptyBlocks: emptyBlocks,
|
||||
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,8 +93,11 @@ class Mining {
|
||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||
const hashrates: any[] = [];
|
||||
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
||||
const lastMidnight = this.getDateMidnight(new Date());
|
||||
let toTimestamp = Math.round((lastMidnight.getTime() - 604800) / 1000);
|
||||
|
||||
const now = new Date();
|
||||
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
|
||||
const lastMondayMidnight = this.getDateMidnight(lastMonday);
|
||||
let toTimestamp = Math.round((lastMondayMidnight.getTime() - 604800) / 1000);
|
||||
|
||||
const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008;
|
||||
let indexedThisRun = 0;
|
||||
@ -146,7 +145,7 @@ class Mining {
|
||||
hashrates.length = 0;
|
||||
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
if (elapsedSeconds > 5) {
|
||||
if (elapsedSeconds > 1) {
|
||||
const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
|
||||
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
|
||||
const weeksLeft = Math.round(totalWeekIndexed - totalIndexed);
|
||||
@ -232,7 +231,7 @@ class Mining {
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
if (elapsedSeconds > 5) {
|
||||
if (elapsedSeconds > 1) {
|
||||
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
|
||||
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
|
||||
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
|
||||
|
34
backend/src/api/rbf-cache.ts
Normal file
34
backend/src/api/rbf-cache.ts
Normal 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();
|
@ -11,6 +11,7 @@ import { Common } from './common';
|
||||
import loadingIndicators from './loading-indicators';
|
||||
import config from '../config';
|
||||
import transactionUtils from './transaction-utils';
|
||||
import rbfCache from './rbf-cache';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@ -48,29 +49,38 @@ class WebsocketHandler {
|
||||
if (parsedMessage && parsedMessage['track-tx']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(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']) {
|
||||
const tx = memPool.getMempool()[client['track-tx']];
|
||||
if (tx) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
response['tx'] = tx;
|
||||
const rbfCacheTx = rbfCache.get(client['track-tx']);
|
||||
if (rbfCacheTx) {
|
||||
response['txReplaced'] = {
|
||||
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 {
|
||||
// tx.prevouts is missing from transactions when in bitcoind mode
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
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));
|
||||
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 {
|
||||
@ -221,14 +231,10 @@ class WebsocketHandler {
|
||||
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mempool = memPool.getMempool();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
delete mempool[rbfTransaction];
|
||||
}
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
|
||||
this.wss.clients.forEach(async (client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
@ -332,27 +338,26 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (client['track-tx']) {
|
||||
const utxoSpent = newTransactions.some((tx) => {
|
||||
return tx.vin.some((vin) => vin.txid === client['track-tx']);
|
||||
});
|
||||
if (utxoSpent) {
|
||||
response['utxoSpent'] = true;
|
||||
const outspends: object = {};
|
||||
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
|
||||
if (vin.txid === client['track-tx']) {
|
||||
outspends[vin.vout] = {
|
||||
vin: i,
|
||||
txid: tx.txid,
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
if (Object.keys(outspends).length) {
|
||||
response['utxoSpent'] = outspends;
|
||||
}
|
||||
|
||||
if (rbfTransactions[client['track-tx']]) {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (client['track-tx'] === rbfTransaction) {
|
||||
const rbfTx = rbfTransactions[rbfTransaction];
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
|
||||
response['rbfTransaction'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
response['rbfTransaction'] = rbfTx;
|
||||
}
|
||||
response['rbfTransaction'] = {
|
||||
txid: rbfTransactions[rbfTransaction].txid,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -414,7 +419,6 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
|
||||
client['track-tx'] = null;
|
||||
response['txConfirmed'] = true;
|
||||
}
|
||||
|
||||
|
@ -299,6 +299,7 @@ class Server {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y'))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y'))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all'))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/hashrate', routes.$getPoolHistoricalHashrate)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks', routes.$getPoolBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/blocks/:height', routes.$getPoolBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
|
||||
|
@ -79,7 +79,7 @@ export interface TransactionStripped {
|
||||
|
||||
export interface BlockExtension {
|
||||
totalFees?: number;
|
||||
medianFee?: number;
|
||||
medianFee?: number; // Actually the median fee rate that we compute ourself
|
||||
feeRange?: number[];
|
||||
reward?: number;
|
||||
coinbaseTx?: TransactionMinerInfo;
|
||||
@ -87,7 +87,10 @@ export interface BlockExtension {
|
||||
pool?: {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
avgFee?: number;
|
||||
avgFeeRate?: number;
|
||||
coinbaseRaw?: string;
|
||||
}
|
||||
|
||||
export interface BlockExtended extends IEsploraApi.Block {
|
||||
|
@ -3,11 +3,6 @@ import { DB } from '../database';
|
||||
import logger from '../logger';
|
||||
import { Common } from '../api/common';
|
||||
|
||||
export interface EmptyBlocks {
|
||||
emptyBlocks: number;
|
||||
poolId: number;
|
||||
}
|
||||
|
||||
class BlocksRepository {
|
||||
/**
|
||||
* Save indexed block data in the database
|
||||
@ -17,17 +12,17 @@ class BlocksRepository {
|
||||
|
||||
try {
|
||||
const query = `INSERT INTO blocks(
|
||||
height, hash, blockTimestamp, size,
|
||||
weight, tx_count, coinbase_raw, difficulty,
|
||||
pool_id, fees, fee_span, median_fee,
|
||||
reward, version, bits, nonce,
|
||||
merkle_root, previous_block_hash
|
||||
height, hash, blockTimestamp, size,
|
||||
weight, tx_count, coinbase_raw, difficulty,
|
||||
pool_id, fees, fee_span, median_fee,
|
||||
reward, version, bits, nonce,
|
||||
merkle_root, previous_block_hash, avg_fee, avg_fee_rate
|
||||
) VALUE (
|
||||
?, ?, FROM_UNIXTIME(?), ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?
|
||||
?, ?, ?, ?
|
||||
)`;
|
||||
|
||||
const params: any[] = [
|
||||
@ -37,21 +32,22 @@ class BlocksRepository {
|
||||
block.size,
|
||||
block.weight,
|
||||
block.tx_count,
|
||||
'',
|
||||
block.extras.coinbaseRaw,
|
||||
block.difficulty,
|
||||
block.extras.pool?.id, // Should always be set to something
|
||||
0,
|
||||
'[]',
|
||||
block.extras.medianFee ?? 0,
|
||||
block.extras.reward ?? 0,
|
||||
block.extras.totalFees,
|
||||
JSON.stringify(block.extras.feeRange),
|
||||
block.extras.medianFee,
|
||||
block.extras.reward,
|
||||
block.version,
|
||||
block.bits,
|
||||
block.nonce,
|
||||
block.merkle_root,
|
||||
block.previousblockhash
|
||||
block.previousblockhash,
|
||||
block.extras.avgFee,
|
||||
block.extras.avgFeeRate,
|
||||
];
|
||||
|
||||
// logger.debug(query);
|
||||
await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e: any) {
|
||||
@ -100,12 +96,13 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get empty blocks for one or all pools
|
||||
*/
|
||||
public async $getEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<EmptyBlocks[]> {
|
||||
public async $countEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<any> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
const params: any[] = [];
|
||||
let query = `SELECT height, hash, tx_count, size, pool_id, weight, UNIX_TIMESTAMP(blockTimestamp) as timestamp
|
||||
let query = `SELECT count(height) as count, pools.id as poolId
|
||||
FROM blocks
|
||||
JOIN pools on pools.id = blocks.pool_id
|
||||
WHERE tx_count = 1`;
|
||||
|
||||
if (poolId) {
|
||||
@ -117,13 +114,14 @@ class BlocksRepository {
|
||||
query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||
}
|
||||
|
||||
// logger.debug(query);
|
||||
query += ` GROUP by pools.id`;
|
||||
|
||||
const connection = await DB.pool.getConnection();
|
||||
try {
|
||||
const [rows] = await connection.query(query, params);
|
||||
connection.release();
|
||||
|
||||
return <EmptyBlocks[]>rows;
|
||||
return rows;
|
||||
} catch (e) {
|
||||
connection.release();
|
||||
logger.err('$getEmptyBlocks() error' + (e instanceof Error ? e.message : e));
|
||||
@ -134,7 +132,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get blocks count for a period
|
||||
*/
|
||||
public async $blockCount(poolId: number | null, interval: string | null): Promise<number> {
|
||||
public async $blockCount(poolId: number | null, interval: string | null = null): Promise<number> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
const params: any[] = [];
|
||||
@ -275,7 +273,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Get one block by height
|
||||
*/
|
||||
public async $getBlockByHeight(height: number): Promise<object | null> {
|
||||
public async $getBlockByHeight(height: number): Promise<object | null> {
|
||||
const connection = await DB.pool.getConnection();
|
||||
try {
|
||||
const [rows]: any[] = await connection.query(`
|
||||
@ -301,7 +299,7 @@ class BlocksRepository {
|
||||
/**
|
||||
* Return blocks difficulty
|
||||
*/
|
||||
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
|
||||
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
const connection = await DB.pool.getConnection();
|
||||
|
@ -116,6 +116,52 @@ class HashratesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pool hashrate history
|
||||
*/
|
||||
public async $getPoolWeeklyHashrate(poolId: number): Promise<any[]> {
|
||||
const connection = await DB.pool.getConnection();
|
||||
|
||||
// Find hashrate boundaries
|
||||
let query = `SELECT MIN(hashrate_timestamp) as firstTimestamp, MAX(hashrate_timestamp) as lastTimestamp
|
||||
FROM hashrates
|
||||
JOIN pools on pools.id = pool_id
|
||||
WHERE hashrates.type = 'weekly' AND pool_id = ? AND avg_hashrate != 0
|
||||
ORDER by hashrate_timestamp LIMIT 1`;
|
||||
|
||||
let boundaries = {
|
||||
firstTimestamp: '1970-01-01',
|
||||
lastTimestamp: '9999-01-01'
|
||||
};
|
||||
try {
|
||||
const [rows]: any[] = await connection.query(query, [poolId]);
|
||||
boundaries = rows[0];
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
connection.release();
|
||||
logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
|
||||
// Get hashrates entries between boundaries
|
||||
query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
|
||||
FROM hashrates
|
||||
JOIN pools on pools.id = pool_id
|
||||
WHERE hashrates.type = 'weekly' AND hashrate_timestamp BETWEEN ? AND ?
|
||||
AND pool_id = ?
|
||||
ORDER by hashrate_timestamp`;
|
||||
|
||||
try {
|
||||
const [rows]: any[] = await connection.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, poolId]);
|
||||
connection.release();
|
||||
|
||||
return rows;
|
||||
} catch (e) {
|
||||
connection.release();
|
||||
logger.err('$getPoolWeeklyHashrate() error' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $setLatestRunTimestamp(key: string, val: any = null) {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `UPDATE state SET number = ? WHERE name = ?`;
|
||||
@ -136,6 +182,9 @@ class HashratesRepository {
|
||||
const [rows] = await connection.query<any>(query, [key]);
|
||||
connection.release();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return rows[0]['number'];
|
||||
} catch (e) {
|
||||
connection.release();
|
||||
|
@ -538,7 +538,7 @@ class Routes {
|
||||
|
||||
public async $getPool(req: Request, res: Response) {
|
||||
try {
|
||||
const stats = await mining.$getPoolStat(req.params.interval ?? null, parseInt(req.params.poolId, 10));
|
||||
const stats = await mining.$getPoolStat(parseInt(req.params.poolId, 10));
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
@ -603,6 +603,22 @@ class Routes {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getPoolHistoricalHashrate(req: Request, res: Response) {
|
||||
try {
|
||||
const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(parseInt(req.params.poolId, 10));
|
||||
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
res.json({
|
||||
oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
|
||||
hashrates: hashrates,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
public async $getHistoricalHashrate(req: Request, res: Response) {
|
||||
try {
|
||||
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
|
||||
@ -665,7 +681,7 @@ class Routes {
|
||||
}
|
||||
|
||||
let nextHash = startFromHash;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 10 && nextHash; i++) {
|
||||
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
|
||||
if (localBlock) {
|
||||
returnBlocks.push(localBlock);
|
||||
|
3
contributors/bosch-0.txt
Normal file
3
contributors/bosch-0.txt
Normal 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
|
@ -75,6 +75,7 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/
|
||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
|
||||
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -131,6 +132,7 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st
|
||||
HashrateChartPoolsComponent,
|
||||
MiningStartComponent,
|
||||
AmountShortenerPipe,
|
||||
DifficultyAdjustmentsTable,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
|
@ -217,12 +217,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.blockSubsidy = 0;
|
||||
return;
|
||||
}
|
||||
this.blockSubsidy = 50;
|
||||
let halvenings = Math.floor(this.block.height / 210000);
|
||||
while (halvenings > 0) {
|
||||
this.blockSubsidy = this.blockSubsidy / 2;
|
||||
halvenings--;
|
||||
}
|
||||
const halvings = Math.floor(this.block.height / 210000);
|
||||
this.blockSubsidy = 50 * 2 ** -halvings;
|
||||
}
|
||||
|
||||
pageChange(page: number, target: HTMLElement) {
|
||||
|
@ -110,7 +110,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
this.markBlockSubscription = this.stateService.markBlock$
|
||||
.subscribe((state) => {
|
||||
this.markHeight = undefined;
|
||||
if (state.blockHeight) {
|
||||
if (state.blockHeight !== undefined) {
|
||||
this.markHeight = state.blockHeight;
|
||||
}
|
||||
this.moveArrowToPosition(false);
|
||||
@ -127,7 +127,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
moveArrowToPosition(animate: boolean, newBlockFromLeft = false) {
|
||||
if (!this.markHeight) {
|
||||
if (this.markHeight === undefined) {
|
||||
this.arrowVisible = false;
|
||||
return;
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
<div>
|
||||
<table class="table latest-transactions" style="min-height: 295px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="d-none d-md-block" i18n="block.height">Height</th>
|
||||
<th i18n="mining.adjusted" class="text-left">Adjusted</th>
|
||||
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
|
||||
<th i18n="mining.change" class="text-right">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||
<tr *ngFor="let diffChange of data.difficulty">
|
||||
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
|
||||
}}</a></td>
|
||||
<td class="text-left">
|
||||
<app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||
{{ diffChange.change >= 0 ? '+' : '' }}{{ formatNumber(diffChange.change, locale, '1.2-2') }}%
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody *ngIf="isLoading">
|
||||
<tr *ngFor="let item of [1,2,3,4,5]">
|
||||
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
|
||||
<td class="text-left"><span class="skeleton-loader w-75"></span></td>
|
||||
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
||||
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -0,0 +1,40 @@
|
||||
.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-adjustments-table',
|
||||
templateUrl: './difficulty-adjustments-table.component.html',
|
||||
styleUrls: ['./difficulty-adjustments-table.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class DifficultyAdjustmentsTable implements OnInit {
|
||||
hashrateObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hashrateObservable$ = this.apiService.getHistoricalHashrate$('1y')
|
||||
.pipe(
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
|
||||
) / 3600 / 24;
|
||||
|
||||
const tableData = [];
|
||||
for (let i = data.difficulty.length - 1; i > 0; --i) {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty);
|
||||
const change = (data.difficulty[i].difficulty / data.difficulty[i - 1].difficulty - 1) * 100;
|
||||
|
||||
tableData.push(Object.assign(data.difficulty[i], {
|
||||
change: change,
|
||||
difficultyShorten: formatNumber(
|
||||
data.difficulty[i].difficulty / selectedPowerOfTen.divider,
|
||||
this.locale, '1.2-2') + selectedPowerOfTen.unit
|
||||
}));
|
||||
}
|
||||
this.isLoading = false;
|
||||
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
difficulty: tableData.slice(0, 5),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<div [class]="widget === false ? 'full-container' : ''">
|
||||
|
||||
<div *ngIf="!tableOnly" class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||
@ -25,31 +25,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="(hashrateObservable$ | async) && !tableOnly" [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'mt-3 p-2' : 'ml-4 mr-4 mt-1'" *ngIf="tableOnly">
|
||||
<table class="table table-borderless table-sm text-left" [class]="widget ? 'compact' : ''">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
|
||||
<th i18n="mining.adjusted">Adjusted</th>
|
||||
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
|
||||
<th i18n="mining.change" class="text-right">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||
<tr *ngFor="let diffChange of data.difficulty">
|
||||
<td class="d-none d-md-block">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
|
||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -48,8 +48,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compact td {
|
||||
padding: 0 !important;
|
||||
margin: 0.15rem !important;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
@ -15,11 +15,12 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 38%;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HashrateChartComponent implements OnInit {
|
||||
@Input() tableOnly = false;
|
||||
@ -45,6 +46,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
@ -92,9 +94,15 @@ export class HashrateChartComponent implements OnInit {
|
||||
|
||||
this.prepareChartOptions({
|
||||
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty])
|
||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
||||
timestamp: data.oldestIndexedBlockTimestamp,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
if (data.hashrates.length === 0) {
|
||||
this.cd.markForCheck();
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
@ -115,9 +123,12 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
difficulty: this.tableOnly ? (this.isMobile() ? tableData.slice(0, 12) : tableData.slice(0, 9)) : tableData
|
||||
difficulty: this.tableOnly ? tableData.slice(0, 5) : tableData,
|
||||
};
|
||||
}),
|
||||
retryWhen((errors) => errors.pipe(
|
||||
delay(60000)
|
||||
))
|
||||
);
|
||||
}),
|
||||
share()
|
||||
@ -125,7 +136,25 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.hashrates.length === 0) {
|
||||
const lastBlock = new Date(data.timestamp * 1000);
|
||||
const dd = String(lastBlock.getDate()).padStart(2, '0');
|
||||
const mm = String(lastBlock.getMonth() + 1).padStart(2, '0'); // January is 0!
|
||||
const yyyy = lastBlock.getFullYear();
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: `Indexing in progess - ${yyyy}-${mm}-${dd}`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
@ -168,18 +197,19 @@ export class HashrateChartComponent implements OnInit {
|
||||
difficulty = Math.round(data[1].data[1] / difficultyPowerOfTen.divider);
|
||||
}
|
||||
|
||||
const date = new Date(data[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
return `
|
||||
<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>
|
||||
<b style="color: white; margin-left: 18px">${date}</b><br>
|
||||
<span>${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s</span><br>
|
||||
<span>${data[1].marker} ${data[1].seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}</span>
|
||||
`;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: {
|
||||
xAxis: data.hashrates.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
},
|
||||
legend: {
|
||||
legend: data.hashrates.length === 0 ? undefined : {
|
||||
data: [
|
||||
{
|
||||
name: 'Hashrate',
|
||||
@ -205,7 +235,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
},
|
||||
],
|
||||
},
|
||||
yAxis: [
|
||||
yAxis: data.hashrates.length === 0 ? undefined : [
|
||||
{
|
||||
min: function (value) {
|
||||
return value.min * 0.9;
|
||||
@ -244,7 +274,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
series: data.hashrates.length === 0 ? [] : [
|
||||
{
|
||||
name: 'Hashrate',
|
||||
showSymbol: false,
|
||||
|
@ -25,7 +25,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
@ -22,7 +22,7 @@ import { poolsColor } from 'src/app/app.constants';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HashrateChartPoolsComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() widget = false;
|
||||
@Input() right: number | string = 40;
|
||||
@Input() left: number | string = 25;
|
||||
|
||||
@ -43,6 +43,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
@ -105,9 +106,15 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series
|
||||
series: series,
|
||||
timestamp: data.oldestIndexedBlockTimestamp,
|
||||
});
|
||||
this.isLoading = false;
|
||||
|
||||
if (series.length === 0) {
|
||||
this.cd.markForCheck();
|
||||
throw new Error();
|
||||
}
|
||||
}),
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
@ -117,6 +124,9 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
};
|
||||
}),
|
||||
retryWhen((errors) => errors.pipe(
|
||||
delay(60000)
|
||||
))
|
||||
);
|
||||
}),
|
||||
share()
|
||||
@ -124,7 +134,25 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
let title: object;
|
||||
if (data.series.length === 0) {
|
||||
const lastBlock = new Date(data.timestamp * 1000);
|
||||
const dd = String(lastBlock.getDate()).padStart(2, '0');
|
||||
const mm = String(lastBlock.getMonth() + 1).padStart(2, '0'); // January is 0!
|
||||
const yyyy = lastBlock.getFullYear();
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: `Indexing in progess - ${yyyy}-${mm}-${dd}`,
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
@ -146,7 +174,8 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (data) {
|
||||
let tooltip = `<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>`;
|
||||
const date = new Date(data[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>`;
|
||||
data.sort((a, b) => b.data[1] - a.data[1]);
|
||||
for (const pool of data) {
|
||||
if (pool.data[1] > 0) {
|
||||
@ -156,14 +185,14 @@ export class HashrateChartPoolsComponent implements OnInit {
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: {
|
||||
xAxis: data.series.length === 0 ? undefined : {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
},
|
||||
legend: (this.isMobile() || this.widget) ? undefined : {
|
||||
legend: (this.isMobile() || this.widget || data.series.length === 0) ? undefined : {
|
||||
data: data.legends
|
||||
},
|
||||
yAxis: {
|
||||
yAxis: data.series.length === 0 ? undefined : {
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
|
@ -109,8 +109,12 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
const height = this.blocks[this.blocks.length - 1].height - 1;
|
||||
if (height < 0) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.electrsApiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1)
|
||||
this.electrsApiService.listBlocks$(height)
|
||||
.subscribe((blocks) => {
|
||||
this.blocks = this.blocks.concat(blocks);
|
||||
this.isLoading = false;
|
||||
|
@ -26,7 +26,7 @@
|
||||
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until>
|
||||
</ng-template>
|
||||
<ng-template #timeDiffMainnet>
|
||||
<app-time-until [time]="(timeAvg * i) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
|
||||
<app-time-until [time]="(timeAvg * i) + now + timeAvg + timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #mergedBlock>
|
||||
|
@ -33,6 +33,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
networkSubscription: Subscription;
|
||||
network = '';
|
||||
now = new Date().getTime();
|
||||
timeOffset = 0;
|
||||
showMiningInfo = false;
|
||||
|
||||
blockWidth = 125;
|
||||
@ -146,6 +147,15 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
timeAvgMins += Math.abs(timeAvgDiff);
|
||||
}
|
||||
|
||||
// testnet difficulty is set to 1 after 20 minutes of no blockSize
|
||||
// therefore the time between blocks will always be below 20 minutes (1200s)
|
||||
if (this.stateService.network === 'testnet' && now - block.timestamp + timeAvgMins * 60 > 1200) {
|
||||
this.timeOffset = -Math.min(now - block.timestamp, 1200) * 1000;
|
||||
timeAvgMins = 20;
|
||||
} else {
|
||||
this.timeOffset = 0;
|
||||
}
|
||||
|
||||
return timeAvgMins * 60 * 1000;
|
||||
})
|
||||
);
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
<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="main-title">Reward stats</div>
|
||||
<div class="card" style="height: 123px">
|
||||
<div class="card-body more-padding">
|
||||
<div class="difficulty-adjustment-container" *ngIf="$rewardStats | async as rewardStats">
|
||||
<div class="fee-estimation-container" *ngIf="$rewardStats | async as rewardStats; else loadingReward">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="">Miners Reward</h5>
|
||||
<div class="card-text">
|
||||
@ -17,7 +18,7 @@
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="">Reward Per Tx</h5>
|
||||
<div class="card-text">
|
||||
{{ rewardStats.rewardPerTx }}
|
||||
{{ rewardStats.rewardPerTx | amountShortener }}
|
||||
<span class="symbol">sats/tx</span>
|
||||
<div class="symbol">in the last 8 blocks</div>
|
||||
</div>
|
||||
@ -25,7 +26,7 @@
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="">Average Fee</h5>
|
||||
<div class="card-text">
|
||||
{{ rewardStats.feePerTx }}
|
||||
{{ rewardStats.feePerTx | amountShortener}}
|
||||
<span class="symbol">sats/tx</span>
|
||||
<div class="symbol">in the last 8 blocks</div>
|
||||
</div>
|
||||
@ -34,6 +35,31 @@
|
||||
</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>
|
||||
|
||||
<!-- difficulty adjustment -->
|
||||
<div class="col">
|
||||
@ -86,9 +112,9 @@
|
||||
<div class="card" style="height: 385px">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Adjusments
|
||||
Adjustments
|
||||
</h5>
|
||||
<app-hashrate-chart [tableOnly]=true [widget]=true></app-hashrate-chart>
|
||||
<app-difficulty-adjustments-table></app-difficulty-adjustments-table>
|
||||
<div class="mt-1"><a [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="dashboard.view-more">View more
|
||||
»</a></div>
|
||||
</div>
|
||||
@ -96,4 +122,4 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
@ -56,39 +56,22 @@
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.general-stats {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
.fee-estimation-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
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;
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
@media (min-width: 785px) {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
&:first-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
@ -98,48 +81,37 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.difficulty-adjustment-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
.shared-block {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
max-width: 90px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
.item {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
&:nth-child(1) {
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
position: relative;
|
||||
&:last-child {
|
||||
margin: 10px auto 3px;
|
||||
max-width: 55px;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnDestroy, OnIni
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@ -22,7 +20,6 @@ export class MiningDashboardComponent implements OnInit {
|
||||
|
||||
constructor(private seoService: SeoService,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
|
||||
@ -39,8 +36,8 @@ export class MiningDashboardComponent implements OnInit {
|
||||
|
||||
return {
|
||||
'totalReward': totalReward,
|
||||
'rewardPerTx': formatNumber(totalReward / totalTx, this.locale, '1.0-0'),
|
||||
'feePerTx': formatNumber(totalFee / totalTx, this.locale, '1.0-0'),
|
||||
'rewardPerTx': totalReward / totalTx,
|
||||
'feePerTx': totalFee / totalTx,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -1,23 +1,25 @@
|
||||
<div [class]="widget === false ? 'container-xl' : ''">
|
||||
|
||||
<div class="pool-distribution" *ngIf="widget && (miningStatsObservable$ | async) as miningStats">
|
||||
<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 *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>
|
||||
|
||||
@ -96,3 +98,27 @@
|
||||
</table>
|
||||
|
||||
</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>
|
@ -102,4 +102,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
|
@ -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 { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
@ -41,6 +41,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
private miningService: MiningService,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -263,8 +264,8 @@ export class PoolRankingComponent implements OnInit {
|
||||
fontSize: 14,
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
borderWidth: 2,
|
||||
borderRadius: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000',
|
||||
},
|
||||
emphasis: {
|
||||
@ -293,7 +294,9 @@ export class PoolRankingComponent implements OnInit {
|
||||
if (e.data.data === 9999) { // "Other"
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/mining/pool/', e.data.data]);
|
||||
this.zone.run(() => {
|
||||
this.router.navigate(['/mining/pool/', e.data.data]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,49 +1,107 @@
|
||||
<div class="container">
|
||||
|
||||
<div *ngIf="poolStats$ | async as poolStats">
|
||||
<h1 class="m-0">
|
||||
<img width="50" src="{{ poolStats['logo'] }}" onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
|
||||
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
|
||||
<h1 class="m-0 mb-2">
|
||||
<img width="50" height="50" src="{{ poolStats['logo'] }}"
|
||||
onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
|
||||
{{ poolStats.pool.name }}
|
||||
</h1>
|
||||
|
||||
<div class="box pl-0 bg-transparent">
|
||||
<div class="card-header mb-0 mb-lg-4 pr-0 pl-0">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup ml-0">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'24h'"> 24h
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3d'"> 3D
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1w'"> 1W
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1m'"> 1M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3m'"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'6m'"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'2y'"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'3y'"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-lg-7">
|
||||
<table class="table table-borderless table-striped" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Tags</td>
|
||||
<td class="text-truncate">
|
||||
<div class="scrollable">
|
||||
{{ poolStats.pool.regexes }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Addresses</td>
|
||||
<td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress">
|
||||
<div class="scrollable">
|
||||
<a *ngFor="let address of poolStats.pool.addresses"
|
||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||
address }}<br></a>
|
||||
</div>
|
||||
</td>
|
||||
<ng-template #noaddress>
|
||||
<td>~</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Mined Blocks</td>
|
||||
<td class="data">{{ formatNumber(poolStats.blockCount, this.locale, '1.0-0') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Empty Blocks</td>
|
||||
<td class="data">{{ formatNumber(poolStats.emptyBlocks, this.locale, '1.0-0') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<table *ngIf="blocks$ | async as blocks" class="table table-borderless" [alwaysCallback]="true" infiniteScroll
|
||||
[infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50"
|
||||
(scrolled)="loadMore()">
|
||||
<thead>
|
||||
<th style="width: 15%;" i18n="latest-blocks.height">Height</th>
|
||||
<th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
|
||||
<th class="text-right" style="width: 10%; padding-right: 30px" i18n="latest-blocks.reward">Reward</th>
|
||||
<th class="d-none d-lg-block text-right" style="width: 15%; padding-right: 40px"
|
||||
i18n="latest-blocks.transactions">
|
||||
Transactions</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td>
|
||||
<td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td>
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td class="text-right" style="padding-right: 30px">
|
||||
<app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td class="d-none d-lg-block text-right" style="padding-right: 40px">{{ block.tx_count | number }}</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" role="progressbar"
|
||||
[ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingMain>
|
||||
<div>
|
||||
<h1 class="m-0 mb-2">
|
||||
<img width="50" height="50" src="./resources/mining-pools/default.svg" class="mr-3">
|
||||
<div class="skeleton-loader"></div>
|
||||
</h1>
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
@ -52,16 +110,20 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-3">Addresses</td>
|
||||
<td class="text-truncate" *ngIf="poolStats.pool.addresses.length else noaddress">
|
||||
<td class="text-truncate">
|
||||
<div class="scrollable">
|
||||
<a *ngFor="let address of poolStats.pool.addresses" [routerLink]="['/address' | relativeUrl, address]">{{ address }}<br></a>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</td>
|
||||
<ng-template #noaddress><td>~</td></ng-template>
|
||||
<ng-template #noaddress>
|
||||
<td>~</td>
|
||||
</ng-template>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-3">Coinbase Tags</td>
|
||||
<td class="text-truncate">{{ poolStats.pool.regexes }}</td>
|
||||
<td class="text-truncate">
|
||||
<div class="skeleton-loader"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -71,11 +133,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-8">Mined Blocks</td>
|
||||
<td class="text-left">{{ poolStats.blockCount }}</td>
|
||||
<td class="text-left">
|
||||
<div class="skeleton-loader"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-4 col-lg-8">Empty Blocks</td>
|
||||
<td class="text-left">{{ poolStats.emptyBlocks.length }}</td>
|
||||
<td class="text-left">
|
||||
<div class="skeleton-loader"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -83,31 +149,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
|
||||
<thead>
|
||||
<th style="width: 15%;" i18n="latest-blocks.height">Height</th>
|
||||
<th class="d-none d-md-block" style="width: 20%;" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.mined">Mined</th>
|
||||
<th style="width: 10%;" i18n="latest-blocks.reward">Reward</th>
|
||||
<th class="d-none d-lg-block" style="width: 15%;" i18n="latest-blocks.transactions">Transactions</th>
|
||||
<th style="width: 20%;" i18n="latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks">
|
||||
<tr *ngFor="let block of blocks">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a></td>
|
||||
<td class="d-none d-md-block">‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
|
||||
<td class=""><app-amount [satoshis]="block['reward']" digitsInfo="1.2-2" [noFiat]="true"></app-amount></td>
|
||||
<td class="d-none d-lg-block">{{ block.tx_count | number }}</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"></div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
@ -18,9 +18,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 830px) {
|
||||
margin-left: 2%;
|
||||
flex-direction: row;
|
||||
float: left;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
@ -37,5 +36,31 @@ div.scrollable {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
max-height: 100px;
|
||||
max-height: 75px;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: 0px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.label {
|
||||
max-width: 50px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: center;
|
||||
@media (max-width: 767.98px) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
@ -1,51 +1,78 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map, switchMap, tap, toArray } from 'rxjs/operators';
|
||||
import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
|
||||
import { formatNumber } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pool',
|
||||
templateUrl: './pool.component.html',
|
||||
styleUrls: ['./pool.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PoolComponent implements OnInit {
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
|
||||
formatNumber = formatNumber;
|
||||
poolStats$: Observable<PoolStat>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
isLoading = true;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
|
||||
fromHeight: number = -1;
|
||||
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromHeight);
|
||||
|
||||
blocks: BlockExtended[] = [];
|
||||
poolId: number = undefined;
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1w' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1w');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.poolStats$ = combineLatest([
|
||||
this.route.params.pipe(map((params) => params.poolId)),
|
||||
this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith('1w')),
|
||||
])
|
||||
this.poolStats$ = this.route.params.pipe(map((params) => params.poolId))
|
||||
.pipe(
|
||||
switchMap((params: any) => {
|
||||
this.poolId = params[0];
|
||||
switchMap((poolId: any) => {
|
||||
this.isLoading = true;
|
||||
this.poolId = poolId;
|
||||
return this.apiService.getPoolHashrate$(this.poolId)
|
||||
.pipe(
|
||||
switchMap((data) => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]));
|
||||
return poolId;
|
||||
}),
|
||||
)
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (this.blocks.length === 0) {
|
||||
this.fromHeightSubject.next(undefined);
|
||||
}
|
||||
return this.apiService.getPoolStats$(this.poolId, params[1] ?? '1w');
|
||||
return this.apiService.getPoolStats$(this.poolId);
|
||||
}),
|
||||
map((poolStats) => {
|
||||
let regexes = '"';
|
||||
@ -74,6 +101,96 @@ export class PoolComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
color: [
|
||||
new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
{ offset: 0.75, color: '#FDD835' },
|
||||
{ offset: 1, color: '#7CB342' }
|
||||
]),
|
||||
'#D81B60',
|
||||
],
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: 60,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (data) {
|
||||
let hashratePowerOfTen: any = selectPowerOfTen(1);
|
||||
let hashrate = data[0].data[1];
|
||||
|
||||
if (this.isMobile()) {
|
||||
hashratePowerOfTen = selectPowerOfTen(data[0].data[1]);
|
||||
hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider);
|
||||
}
|
||||
|
||||
return `
|
||||
<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>
|
||||
<span>${data[0].marker} ${data[0].seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s</span><br>
|
||||
`;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile()) ? 5 : 10,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
min: function (value) {
|
||||
return value.min * 0.9;
|
||||
},
|
||||
type: 'value',
|
||||
name: 'Hashrate',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Hashrate',
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.fromHeightSubject.next(this.blocks[this.blocks.length - 1]?.height);
|
||||
}
|
||||
|
@ -3,13 +3,13 @@
|
||||
<div class="title-block">
|
||||
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
||||
<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-none d-lg-inline">{{ rbfTransaction.txid }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ng-container>
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
<span class="tx-link float-left">
|
||||
|
@ -37,6 +37,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
txReplacedSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
showCpfpDetails = false;
|
||||
@ -217,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;
|
||||
|
||||
if (txConfirmed && this.tx) {
|
||||
@ -232,9 +234,13 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
this.stateService.txReplaced$.subscribe(
|
||||
(rbfTransaction) => (this.rbfTransaction = rbfTransaction)
|
||||
);
|
||||
this.txReplacedSubscription = this.stateService.txReplaced$.subscribe((rbfTransaction) => {
|
||||
if (!this.tx) {
|
||||
this.error = new Error();
|
||||
this.waitingForTransaction = false;
|
||||
}
|
||||
this.rbfTransaction = rbfTransaction;
|
||||
});
|
||||
}
|
||||
|
||||
handleLoadElectrsTransactionError(error: any): Observable<any> {
|
||||
@ -302,6 +308,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -186,16 +186,16 @@
|
||||
<app-amount [satoshis]="vout.value"></app-amount>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="arrow-td" *ngIf="{ value: (outspends$ | async) } as outspends">
|
||||
<span *ngIf="!outspends.value || !outspends.value[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
||||
<td class="arrow-td">
|
||||
<span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #outspend>
|
||||
<span *ngIf="!outspends.value[i][vindex] || !outspends.value[i][vindex].spent; else spent" class="green">
|
||||
<span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #spent>
|
||||
<a *ngIf="outspends.value[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends.value[i][vindex].txid]" class="red">
|
||||
<a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<ng-template #outputNoTxId>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge } from 'rxjs';
|
||||
import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge, of, Subject, Subscription } from 'rxjs';
|
||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { map, share, switchMap } from 'rxjs/operators';
|
||||
import { map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
@ -27,41 +27,18 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
latestBlock$: Observable<BlockExtended>;
|
||||
outspends$: Observable<Outspend[]>;
|
||||
outspendsSubscription: Subscription;
|
||||
refreshOutspends$: ReplaySubject<object> = new ReplaySubject();
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
_outspends: Outspend[] = [];
|
||||
outspends: Outspend[][] = [];
|
||||
assetsMinimal: any;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private assetsService: AssetsService,
|
||||
) {
|
||||
this.outspends$ = merge(
|
||||
this.refreshOutspends$,
|
||||
this.stateService.utxoSpent$
|
||||
.pipe(
|
||||
map(() => {
|
||||
this._outspends = [];
|
||||
return { 0: this.electrsApiService.getOutspends$(this.transactions[0].txid) };
|
||||
}),
|
||||
)
|
||||
).pipe(
|
||||
switchMap((observableObject) => forkJoin(observableObject)),
|
||||
map((outspends: any) => {
|
||||
const newOutspends = [];
|
||||
for (const i in outspends) {
|
||||
if (outspends.hasOwnProperty(i)) {
|
||||
newOutspends.push(outspends[i]);
|
||||
}
|
||||
}
|
||||
this._outspends = this._outspends.concat(newOutspends);
|
||||
return this._outspends;
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
private ref: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||
@ -72,6 +49,34 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
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() {
|
||||
@ -90,7 +95,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.transactions.forEach((tx, i) => {
|
||||
tx['@voutLimit'] = true;
|
||||
tx['@vinLimit'] = true;
|
||||
if (this._outspends[i]) {
|
||||
if (this.outspends[i]) {
|
||||
return;
|
||||
}
|
||||
observableObject[i] = this.electrsApiService.getOutspends$(tx.txid);
|
||||
@ -149,4 +154,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.showDetails$.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.outspendsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
@ -144,6 +144,9 @@ export class DashboardComponent implements OnInit {
|
||||
this.latestBlockHeight = block.height;
|
||||
}),
|
||||
scan((acc, [block]) => {
|
||||
if (acc.find((b) => b.height == block.height)) {
|
||||
return acc;
|
||||
}
|
||||
acc.unshift(block);
|
||||
acc = acc.slice(0, 6);
|
||||
return acc;
|
||||
@ -153,6 +156,9 @@ export class DashboardComponent implements OnInit {
|
||||
this.transactions$ = this.stateService.transactions$
|
||||
.pipe(
|
||||
scan((acc, tx) => {
|
||||
if (acc.find((t) => t.txid == tx.txid)) {
|
||||
return acc;
|
||||
}
|
||||
acc.unshift(tx);
|
||||
acc = acc.slice(0, 6);
|
||||
return acc;
|
||||
|
@ -90,7 +90,7 @@ export interface PoolInfo {
|
||||
export interface PoolStat {
|
||||
pool: PoolInfo;
|
||||
blockCount: number;
|
||||
emptyBlocks: BlockExtended[];
|
||||
emptyBlocks: number;
|
||||
}
|
||||
|
||||
export interface BlockExtension {
|
||||
|
@ -15,8 +15,9 @@ export interface WebsocketResponse {
|
||||
action?: string;
|
||||
data?: string[];
|
||||
tx?: Transaction;
|
||||
rbfTransaction?: Transaction;
|
||||
utxoSpent?: boolean;
|
||||
rbfTransaction?: ReplacedTransaction;
|
||||
txReplaced?: ReplacedTransaction;
|
||||
utxoSpent?: object;
|
||||
transactions?: TransactionStripped[];
|
||||
loadingIndicators?: ILoadingIndicators;
|
||||
backendInfo?: IBackendInfo;
|
||||
@ -27,6 +28,9 @@ export interface WebsocketResponse {
|
||||
'track-bisq-market'?: string;
|
||||
}
|
||||
|
||||
export interface ReplacedTransaction extends Transaction {
|
||||
txid: string;
|
||||
}
|
||||
export interface MempoolBlock {
|
||||
blink?: boolean;
|
||||
height?: number;
|
||||
|
@ -136,11 +136,12 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getPoolStats$(poolId: number, interval: string | undefined): Observable<PoolStat> {
|
||||
return this.httpClient.get<PoolStat>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}` +
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
getPoolStats$(poolId: number): Observable<PoolStat> {
|
||||
return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}`);
|
||||
}
|
||||
|
||||
getPoolHashrate$(poolId: number): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${poolId}/hashrate`);
|
||||
}
|
||||
|
||||
getPoolBlocks$(poolId: number, fromHeight: number): Observable<BlockExtended[]> {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||
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 { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
@ -71,7 +71,7 @@ export class StateService {
|
||||
network = '';
|
||||
blockVSize: number;
|
||||
env: Env;
|
||||
latestBlockHeight = 0;
|
||||
latestBlockHeight = -1;
|
||||
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
blocks$: ReplaySubject<[BlockExtended, boolean]>;
|
||||
@ -80,8 +80,8 @@ export class StateService {
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
txReplaced$ = new Subject<Transaction>();
|
||||
utxoSpent$ = new Subject<null>();
|
||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||
utxoSpent$ = new Subject<object>();
|
||||
mempoolTransactions$ = new Subject<Transaction>();
|
||||
blockTransactions$ = new Subject<Transaction>();
|
||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||
|
@ -68,7 +68,7 @@ export class WebsocketService {
|
||||
clearTimeout(this.onlineCheckTimeout);
|
||||
clearTimeout(this.onlineCheckTimeoutTwo);
|
||||
|
||||
this.stateService.latestBlockHeight = 0;
|
||||
this.stateService.latestBlockHeight = -1;
|
||||
|
||||
this.websocketSubject.complete();
|
||||
this.subscription.unsubscribe();
|
||||
@ -239,6 +239,10 @@ export class WebsocketService {
|
||||
this.stateService.txReplaced$.next(response.rbfTransaction);
|
||||
}
|
||||
|
||||
if (response.txReplaced) {
|
||||
this.stateService.txReplaced$.next(response.txReplaced);
|
||||
}
|
||||
|
||||
if (response['mempool-blocks']) {
|
||||
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
|
||||
}
|
||||
@ -252,7 +256,7 @@ export class WebsocketService {
|
||||
}
|
||||
|
||||
if (response.utxoSpent) {
|
||||
this.stateService.utxoSpent$.next();
|
||||
this.stateService.utxoSpent$.next(response.utxoSpent);
|
||||
}
|
||||
|
||||
if (response.backendInfo) {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 19 KiB |
@ -50,6 +50,7 @@ $dropdown-link-active-bg: #11131f;
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -38,5 +38,13 @@ do for url in / \
|
||||
curl -s "https://${hostname}${url}" >/dev/null
|
||||
done
|
||||
|
||||
counter=1
|
||||
while [ $counter -le 134 ]
|
||||
do
|
||||
curl -s "https://${hostname}/api/v1/mining/pool/${counter}/hashrate" >/dev/null
|
||||
curl -s "https://${hostname}/api/v1/mining/pool/${counter}" >/dev/null
|
||||
((counter++))
|
||||
done
|
||||
|
||||
sleep 10
|
||||
done
|
||||
|
Loading…
x
Reference in New Issue
Block a user