Merge branch 'master' into fee-visibility

This commit is contained in:
Antoni Spaanderman 2022-02-28 13:10:12 +01:00 committed by GitHub
commit 590170a0df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 5463 additions and 5677 deletions

View File

@ -91,11 +91,11 @@ JSON:
"PRICE_FEED_UPDATE_INTERVAL": 600, "PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false, "USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"], "EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"],
"STDOUT_LOG_MIN_PRIORITY": "debug" "STDOUT_LOG_MIN_PRIORITY": "info"
}, },
``` ```
docker-compose overrides:: docker-compose overrides:
``` ```
MEMPOOL_NETWORK: "" MEMPOOL_NETWORK: ""
MEMPOOL_BACKEND: "" MEMPOOL_BACKEND: ""

View File

@ -20,6 +20,7 @@ class Blocks {
private previousDifficultyRetarget = 0; private previousDifficultyRetarget = 0;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private blockIndexingStarted = false; private blockIndexingStarted = false;
public blockIndexingCompleted = false;
constructor() { } constructor() { }
@ -115,6 +116,9 @@ class Blocks {
Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0; Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
blockExtended.extras.feeRange = transactionsTmp.length > 0 ? blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
Common.getFeesInRange(transactionsTmp, 8) : [0, 0]; Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
blockExtended.extras.totalFees = transactionsTmp.reduce((acc, tx) => {
return acc + tx.fee;
}, 0)
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
let pool: PoolTag; let pool: PoolTag;
@ -170,10 +174,7 @@ class Blocks {
* Index all blocks metadata for the mining dashboard * Index all blocks metadata for the mining dashboard
*/ */
public async $generateBlockDatabase() { public async $generateBlockDatabase() {
if (this.blockIndexingStarted === true || if (this.blockIndexingStarted) {
!Common.indexingEnabled() ||
memPool.hasPriority()
) {
return; return;
} }
@ -243,6 +244,8 @@ class Blocks {
logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e); logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e);
console.log(e); console.log(e);
} }
this.blockIndexingCompleted = true;
} }
public async $updateBlocks() { public async $updateBlocks() {

View File

@ -6,7 +6,7 @@ import logger from '../logger';
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 6; private static currentVersion = 8;
private queryTimeout = 120000; private queryTimeout = 120000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
@ -15,13 +15,13 @@ class DatabaseMigration {
* Entry point * Entry point
*/ */
public async $initializeOrMigrateDatabase(): Promise<void> { public async $initializeOrMigrateDatabase(): Promise<void> {
logger.info('MIGRATIONS: Running migrations'); logger.debug('MIGRATIONS: Running migrations');
await this.$printDatabaseVersion(); await this.$printDatabaseVersion();
// First of all, if the `state` database does not exist, create it so we can track migration version // First of all, if the `state` database does not exist, create it so we can track migration version
if (!await this.$checkIfTableExists('state')) { if (!await this.$checkIfTableExists('state')) {
logger.info('MIGRATIONS: `state` table does not exist. Creating it.'); logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
try { try {
await this.$createMigrationStateTable(); await this.$createMigrationStateTable();
} catch (e) { } catch (e) {
@ -29,7 +29,7 @@ class DatabaseMigration {
await sleep(10000); await sleep(10000);
process.exit(-1); process.exit(-1);
} }
logger.info('MIGRATIONS: `state` table initialized.'); logger.debug('MIGRATIONS: `state` table initialized.');
} }
let databaseSchemaVersion = 0; let databaseSchemaVersion = 0;
@ -41,10 +41,10 @@ class DatabaseMigration {
process.exit(-1); process.exit(-1);
} }
logger.info('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion); logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
logger.info('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion); logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) { if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
logger.info('MIGRATIONS: Nothing to do.'); logger.debug('MIGRATIONS: Nothing to do.');
return; return;
} }
@ -58,10 +58,10 @@ class DatabaseMigration {
} }
if (DatabaseMigration.currentVersion > databaseSchemaVersion) { if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
logger.info('MIGRATIONS: Upgrading datababse schema'); logger.notice('MIGRATIONS: Upgrading datababse schema');
try { try {
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion); await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
logger.info(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`); logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
} catch (e) { } catch (e) {
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e); logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
} }
@ -92,11 +92,13 @@ class DatabaseMigration {
await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
} }
if (databaseSchemaVersion < 5 && isBitcoin === true) { if (databaseSchemaVersion < 5 && 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, 'TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
} }
if (databaseSchemaVersion < 6 && isBitcoin === true) { if (databaseSchemaVersion < 6 && 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, 'TRUNCATE blocks;'); // Need to re-index
// Cleanup original blocks fields type // Cleanup original blocks fields type
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
@ -116,6 +118,21 @@ class DatabaseMigration {
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
} }
if (databaseSchemaVersion < 7 && isBitcoin === true) {
await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;');
await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
}
if (databaseSchemaVersion < 8 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
}
connection.release(); connection.release();
} catch (e) { } catch (e) {
connection.release(); connection.release();
@ -143,10 +160,10 @@ class DatabaseMigration {
WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`; WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
const [rows] = await this.$executeQuery(connection, query, true); const [rows] = await this.$executeQuery(connection, query, true);
if (rows[0].hasIndex === 0) { if (rows[0].hasIndex === 0) {
logger.info('MIGRATIONS: `statistics.added` is not indexed'); logger.debug('MIGRATIONS: `statistics.added` is not indexed');
this.statisticsAddedIndexed = false; this.statisticsAddedIndexed = false;
} else if (rows[0].hasIndex === 1) { } else if (rows[0].hasIndex === 1) {
logger.info('MIGRATIONS: `statistics.added` is already indexed'); logger.debug('MIGRATIONS: `statistics.added` is already indexed');
this.statisticsAddedIndexed = true; this.statisticsAddedIndexed = true;
} }
} catch (e) { } catch (e) {
@ -164,7 +181,7 @@ class DatabaseMigration {
*/ */
private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> { private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> {
if (!silent) { if (!silent) {
logger.info('MIGRATIONS: Execute query:\n' + query); logger.debug('MIGRATIONS: Execute query:\n' + query);
} }
return connection.query<any>({ sql: query, timeout: this.queryTimeout }); return connection.query<any>({ sql: query, timeout: this.queryTimeout });
} }
@ -255,6 +272,10 @@ class DatabaseMigration {
} }
} }
if (version < 7) {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
}
return queries; return queries;
} }
@ -272,9 +293,9 @@ class DatabaseMigration {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
try { try {
const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true); const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true);
logger.info(`MIGRATIONS: Database engine version '${rows[0].version}'`); logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
} catch (e) { } catch (e) {
logger.info(`MIGRATIONS: Could not fetch database engine version. ` + e); logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
} }
connection.release(); connection.release();
} }
@ -398,6 +419,40 @@ class DatabaseMigration {
FOREIGN KEY (pool_id) REFERENCES pools (id) FOREIGN KEY (pool_id) REFERENCES pools (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
private getCreateDailyStatsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS hashrates (
hashrate_timestamp timestamp NOT NULL,
avg_hashrate double unsigned DEFAULT '0',
pool_id smallint unsigned NULL,
PRIMARY KEY (hashrate_timestamp),
INDEX (pool_id),
FOREIGN KEY (pool_id) REFERENCES pools (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates'];
const connection = await DB.pool.getConnection();
try {
for (const table of tables) {
if (!allowedTables.includes(table)) {
logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`);
continue;
};
await this.$executeQuery(connection, `TRUNCATE ${table}`, true);
if (table === 'hashrates') {
await this.$executeQuery(connection, 'UPDATE state set number = 0 where name = "last_hashrates_indexing"', true);
}
logger.notice(`Table ${table} has been truncated`);
}
} catch (e) {
logger.warn(`Unable to erase indexed data`);
}
connection.release();
}
} }
export default new DatabaseMigration(); export default new DatabaseMigration();

View File

@ -1,16 +1,21 @@
import { PoolInfo, PoolStats } from '../mempool.interfaces'; import { PoolInfo, PoolStats } from '../mempool.interfaces';
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository'; import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import blocks from './blocks';
class Mining { class Mining {
hashrateIndexingStarted = false;
constructor() { constructor() {
} }
/** /**
* Generate high level overview of the pool ranks and general stats * Generate high level overview of the pool ranks and general stats
*/ */
public async $getPoolsStats(interval: string | null) : Promise<object> { public async $getPoolsStats(interval: string | null): Promise<object> {
const poolsStatistics = {}; const poolsStatistics = {};
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval); const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
@ -26,8 +31,8 @@ class Mining {
link: poolInfo.link, link: poolInfo.link,
blockCount: poolInfo.blockCount, blockCount: poolInfo.blockCount,
rank: rank++, rank: rank++,
emptyBlocks: 0, emptyBlocks: 0
} };
for (let i = 0; i < emptyBlocks.length; ++i) { for (let i = 0; i < emptyBlocks.length; ++i) {
if (emptyBlocks[i].poolId === poolInfo.poolId) { if (emptyBlocks[i].poolId === poolInfo.poolId) {
poolStat.emptyBlocks++; poolStat.emptyBlocks++;
@ -37,15 +42,13 @@ class Mining {
}); });
poolsStatistics['pools'] = poolsStats; poolsStatistics['pools'] = poolsStats;
poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
const blockCount: number = await BlocksRepository.$blockCount(null, interval); const blockCount: number = await BlocksRepository.$blockCount(null, interval);
poolsStatistics['blockCount'] = blockCount; poolsStatistics['blockCount'] = blockCount;
const blockHeightTip = await bitcoinClient.getBlockCount(); const blockHeightTip = await bitcoinClient.getBlockCount();
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip); const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip);
poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate; poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
return poolsStatistics; return poolsStatistics;
@ -74,13 +77,137 @@ class Mining {
* Return the historical difficulty adjustments and oldest indexed block timestamp * Return the historical difficulty adjustments and oldest indexed block timestamp
*/ */
public async $getHistoricalDifficulty(interval: string | null): Promise<object> { public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval); return await BlocksRepository.$getBlocksDifficulty(interval);
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp()); }
return { /**
adjustments: difficultyAdjustments, * Return the historical hashrates and oldest indexed block timestamp
oldestIndexedBlockTimestamp: oldestBlock.getTime(), */
public async $getNetworkHistoricalHashrates(interval: string | null): Promise<object> {
return await HashratesRepository.$getNetworkDailyHashrate(interval);
}
/**
* Return the historical hashrates and oldest indexed block timestamp for one or all pools
*/
public async $getPoolsHistoricalHashrates(interval: string | null, poolId: number): Promise<object> {
return await HashratesRepository.$getPoolsWeeklyHashrate(interval);
}
/**
* Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
// We only run this once a day
const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp();
const now = new Date().getTime() / 1000;
if (now - latestTimestamp < 86400) {
return;
} }
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
return;
}
this.hashrateIndexingStarted = true;
logger.info(`Indexing hashrates`);
const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
let startedAt = new Date().getTime() / 1000;
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMidnight = new Date();
lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0);
let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
let indexedThisRun = 0;
let totalIndexed = 0;
const hashrates: any[] = [];
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400;
if (indexedTimestamp.includes(fromTimestamp)) {
toTimestamp -= 86400;
++totalIndexed;
continue;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp, toTimestamp);
if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
break;
}
let lastBlockHashrate = 0;
lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
if (totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate
logger.debug("Indexing weekly hashrates for mining pools");
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp);
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
pools = pools.map((pool: any) => {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
pool.share = (pool.blockCount / totalBlocks);
return pool;
});
for (const pool of pools) {
hashrates.push({
hashrateTimestamp: fromTimestamp + 1,
avgHashrate: pool['hashrate'],
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
});
}
}
hashrates.push({
hashrateTimestamp: fromTimestamp,
avgHashrate: lastBlockHashrate,
poolId: null,
share: 1,
type: 'daily',
});
if (hashrates.length > 10) {
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
if (elapsedSeconds > 5) {
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
startedAt = new Date().getTime() / 1000;
indexedThisRun = 0;
}
toTimestamp -= 86400;
++indexedThisRun;
++totalIndexed;
}
// Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
hashrates.push({
hashrateTimestamp: genesisTimestamp,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null,
type: 'daily',
});
}
if (hashrates.length > 0) {
await HashratesRepository.$saveHashrates(hashrates);
}
await HashratesRepository.$setLatestRunTimestamp();
this.hashrateIndexingStarted = false;
logger.info(`Hashrates indexing completed`);
} }
} }

View File

@ -26,6 +26,8 @@ import poolsParser from './api/pools-parser';
import syncAssets from './sync-assets'; import syncAssets from './sync-assets';
import icons from './api/liquid/icons'; import icons from './api/liquid/icons';
import { Common } from './api/common'; import { Common } from './api/common';
import mining from './api/mining';
import HashratesRepository from './repositories/HashratesRepository';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -88,6 +90,13 @@ class Server {
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
await checkDbConnection(); await checkDbConnection();
try { try {
if (process.env.npm_config_reindex != undefined) { // Re-index requests
const tables = process.env.npm_config_reindex.split(',');
logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds from now (using '--reindex') ...`);
await Common.sleep(5000);
await databaseMigration.$truncateIndexedData(tables);
}
await this.$resetHashratesIndexingState();
await databaseMigration.$initializeOrMigrateDatabase(); await databaseMigration.$initializeOrMigrateDatabase();
await poolsParser.migratePoolsJson(); await poolsParser.migratePoolsJson();
} catch (e) { } catch (e) {
@ -138,7 +147,7 @@ class Server {
} }
await blocks.$updateBlocks(); await blocks.$updateBlocks();
await memPool.$updateMempool(); await memPool.$updateMempool();
blocks.$generateBlockDatabase(); this.$runIndexingWhenReady();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.currentBackendRetryInterval = 5; this.currentBackendRetryInterval = 5;
@ -157,6 +166,23 @@ class Server {
} }
} }
async $resetHashratesIndexingState() {
return await HashratesRepository.$setLatestRunTimestamp(0);
}
async $runIndexingWhenReady() {
if (!Common.indexingEnabled() || mempool.hasPriority()) {
return;
}
try {
await blocks.$generateBlockDatabase();
await mining.$generateNetworkHashrateHistory();
} catch (e) {
logger.err(`Unable to run indexing right now, trying again later. ` + e);
}
}
setUpWebsocketHandling() { setUpWebsocketHandling() {
if (this.wss) { if (this.wss) {
websocketHandler.setWebsocketServer(this.wss); websocketHandler.setWebsocketServer(this.wss);
@ -276,7 +302,12 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty); .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
;
} }
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {

View File

@ -78,6 +78,7 @@ export interface TransactionStripped {
} }
export interface BlockExtension { export interface BlockExtension {
totalFees?: number;
medianFee?: number; medianFee?: number;
feeRange?: number[]; feeRange?: number[];
reward?: number; reward?: number;

View File

@ -149,16 +149,49 @@ class BlocksRepository {
return <number>rows[0].blockCount; return <number>rows[0].blockCount;
} }
/**
* Get blocks count between two dates
* @param poolId
* @param from - The oldest timestamp
* @param to - The newest timestamp
* @returns
*/
public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise<number> {
const params: any[] = [];
let query = `SELECT
count(height) as blockCount,
max(height) as lastBlockHeight
FROM blocks`;
if (poolId) {
query += ` WHERE pool_id = ?`;
params.push(poolId);
}
if (poolId) {
query += ` AND`;
} else {
query += ` WHERE`;
}
query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`;
// logger.debug(query);
const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, params);
connection.release();
return <number>rows[0];
}
/** /**
* Get the oldest indexed block * Get the oldest indexed block
*/ */
public async $oldestBlockTimestamp(): Promise<number> { public async $oldestBlockTimestamp(): Promise<number> {
const query = `SELECT blockTimestamp const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
FROM blocks FROM blocks
ORDER BY height ORDER BY height
LIMIT 1;`; LIMIT 1;`;
// logger.debug(query); // logger.debug(query);
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(query); const [rows]: any[] = await connection.query(query);
@ -232,21 +265,54 @@ class BlocksRepository {
const connection = await DB.pool.getConnection(); const connection = await DB.pool.getConnection();
let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height // :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
FROM blocks`; // Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) { if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
} }
query += ` GROUP BY difficulty query += `
ORDER BY blockTimestamp DESC`; ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
const [rows]: any[] = await connection.query(query); const [rows]: any[] = await connection.query(query);
connection.release(); connection.release();
for (let row of rows) {
delete row['rn'];
}
return rows; return rows;
} }
public async $getOldestIndexedBlockHeight(): Promise<number> {
const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
connection.release();
return rows[0].minHeight;
}
} }
export default new BlocksRepository(); export default new BlocksRepository();

View File

@ -0,0 +1,102 @@
import { Common } from '../api/common';
import { DB } from '../database';
import logger from '../logger';
import PoolsRepository from './PoolsRepository';
class HashratesRepository {
/**
* Save indexed block data in the database
*/
public async $saveHashrates(hashrates: any) {
let query = `INSERT INTO
hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
for (const hashrate of hashrates) {
query += ` (FROM_UNIXTIME(${hashrate.hashrateTimestamp}), ${hashrate.avgHashrate}, ${hashrate.poolId}, ${hashrate.share}, "${hashrate.type}"),`;
}
query = query.slice(0, -1);
const connection = await DB.pool.getConnection();
try {
// logger.debug(query);
await connection.query(query);
} catch (e: any) {
logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e));
}
connection.release();
}
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
const connection = await DB.pool.getConnection();
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
FROM hashrates`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'daily'
AND pool_id IS NULL`;
} else {
query += ` WHERE hashrates.type = 'daily'
AND pool_id IS NULL`;
}
query += ` ORDER by hashrate_timestamp`;
const [rows]: any[] = await connection.query(query);
connection.release();
return rows;
}
/**
* Returns the current biggest pool hashrate history
*/
public async $getPoolsWeeklyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
const connection = await DB.pool.getConnection();
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
let 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`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'weekly'
AND pool_id IN (${topPoolsId})`;
} else {
query += ` WHERE hashrates.type = 'weekly'
AND pool_id IN (${topPoolsId})`;
}
query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
const [rows]: any[] = await connection.query(query);
connection.release();
return rows;
}
public async $setLatestRunTimestamp(val: any = null) {
const connection = await DB.pool.getConnection();
const query = `UPDATE state SET number = ? WHERE name = 'last_hashrates_indexing'`;
await connection.query<any>(query, (val === null) ? [Math.round(new Date().getTime() / 1000)] : [val]);
connection.release();
}
public async $getLatestRunTimestamp(): Promise<number> {
const connection = await DB.pool.getConnection();
const query = `SELECT number FROM state WHERE name = 'last_hashrates_indexing'`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows[0]['number'];
}
}
export default new HashratesRepository();

View File

@ -49,6 +49,22 @@ class PoolsRepository {
return <PoolInfo[]>rows; return <PoolInfo[]>rows;
} }
/**
* Get basic pool info and block count between two timestamp
*/
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
FROM pools
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
GROUP BY pools.id`;
const connection = await DB.pool.getConnection();
const [rows] = await connection.query(query, [from, to]);
connection.release();
return <PoolInfo[]>rows;
}
/** /**
* Get mining pool statistics for one pool * Get mining pool statistics for one pool
*/ */

View File

@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser';
import icons from './api/liquid/icons'; import icons from './api/liquid/icons';
import miningStats from './api/mining'; import miningStats from './api/mining';
import axios from 'axios'; import axios from 'axios';
import PoolsRepository from './repositories/PoolsRepository';
import mining from './api/mining'; import mining from './api/mining';
import BlocksRepository from './repositories/BlocksRepository'; import BlocksRepository from './repositories/BlocksRepository';
@ -587,6 +586,40 @@ class Routes {
} }
} }
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, 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 mining.$getNetworkHistoricalHashrates(req.params.interval ?? null);
const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
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,
difficulty: difficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlock(req: Request, res: Response) { public async getBlock(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getBlock(req.params.hash); const result = await bitcoinApi.$getBlock(req.params.hash);

View File

@ -17,7 +17,7 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100}
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600} __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false} __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]} __MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=debug} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}

View File

@ -66,7 +66,7 @@ describe('Mainnet', () => {
cy.get('[id^="bitcoin-block-"]').should('have.length', 8); cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
cy.get('.footer').should('be.visible'); cy.get('.footer').should('be.visible');
cy.get('.row > :nth-child(1)').invoke('text').then((text) => { cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
expect(text).to.match(/Tx vBytes per second:.* vB\/s/); expect(text).to.match(/Incoming transactions.* vB\/s/);
}); });
cy.get('.row > :nth-child(2)').invoke('text').then((text) => { cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
expect(text).to.match(/Unconfirmed:(.*)/); expect(text).to.match(/Unconfirmed:(.*)/);

File diff suppressed because it is too large Load Diff

View File

@ -59,39 +59,38 @@
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record", "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open", "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
}, },
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^13.1.2", "@angular-devkit/build-angular": "^13.2.4",
"@angular/animations": "~13.1.1", "@angular/animations": "~13.2.3",
"@angular/cli": "~13.0.4", "@angular/cli": "~13.2.4",
"@angular/common": "~13.1.1", "@angular/common": "~13.2.3",
"@angular/compiler": "~13.1.1", "@angular/compiler": "~13.2.3",
"@angular/core": "~13.1.1", "@angular/core": "~13.2.3",
"@angular/forms": "~13.1.1", "@angular/forms": "~13.2.3",
"@angular/localize": "^13.1.1", "@angular/localize": "^13.2.3",
"@angular/platform-browser": "~13.1.1", "@angular/platform-browser": "~13.2.3",
"@angular/platform-browser-dynamic": "~13.1.1", "@angular/platform-browser-dynamic": "~13.2.3",
"@angular/platform-server": "~13.1.1", "@angular/platform-server": "~13.2.3",
"@angular/router": "~13.1.1", "@angular/router": "~13.2.3",
"@fortawesome/angular-fontawesome": "^0.8.2", "@fortawesome/angular-fontawesome": "0.10.1",
"@fortawesome/fontawesome-common-types": "^0.2.35", "@fortawesome/fontawesome-common-types": "0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "6.0.0",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@nguniversal/express-engine": "11.2.1", "@nguniversal/express-engine": "12.1.3",
"@types/qrcode": "1.4.1", "@types/qrcode": "1.4.1",
"bootstrap": "4.5.0", "bootstrap": "4.5.0",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "^5.1.2", "echarts": "5.3.0",
"express": "^4.17.1", "express": "^4.17.1",
"lightweight-charts": "^3.3.0", "lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0", "ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "^7.0.1", "ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0", "qrcode": "1.5.0",
"rxjs": "^6.6.7", "rxjs": "^6.6.7",
@ -101,9 +100,9 @@
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "~13.1.1", "@angular/compiler-cli": "~13.2.3",
"@angular/language-service": "~13.1.1", "@angular/language-service": "~13.2.3",
"@nguniversal/builders": "^11.2.1", "@nguniversal/builders": "~13.0.2",
"@types/express": "^4.17.0", "@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
@ -123,10 +122,10 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^1.3.0", "@cypress/schematic": "^1.3.0",
"cypress": "^9.3.1", "cypress": "^9.5.0",
"cypress-fail-on-console-error": "^2.1.3", "cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1", "cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3", "mock-socket": "^9.0.3",
"start-server-and-test": "^1.12.6" "start-server-and-test": "^1.12.6"
} }
} }

View File

@ -42,7 +42,28 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
pathRewrite: { pathRewrite: {
"^/liquid/api/": "/api/v1/" "^/liquid/api/": "/api/v1/"
}, },
} },
{
context: ['/liquidtestnet/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet": ""
},
},
{
context: ['/liquidtestnet/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet/api/": "/api/v1/"
},
},
]); ]);
} }

View File

@ -40,6 +40,24 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
changeOrigin: true, changeOrigin: true,
proxyTimeout: 30000, proxyTimeout: 30000,
}, },
{
context: ['/liquidtestnet/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/liquidtestnet": ""
},
},
{
context: ['/liquidtestnet/api/**'],
target: `https://liquid.network`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
]); ]);
} }

View File

@ -28,7 +28,9 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
import { AssetsComponent } from './components/assets/assets.component'; import { AssetsComponent } from './components/assets/assets.component';
import { PoolComponent } from './components/pool/pool.component'; import { PoolComponent } from './components/pool/pool.component';
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component';
let routes: Routes = [ let routes: Routes = [
{ {
@ -70,16 +72,31 @@ let routes: Routes = [
component: LatestBlocksComponent, component: LatestBlocksComponent,
}, },
{ {
path: 'mining/difficulty', path: 'mining',
component: DifficultyChartComponent, component: MiningStartComponent,
}, children: [
{ {
path: 'mining/pools', path: 'hashrate',
component: PoolRankingComponent, component: HashrateChartComponent,
}, },
{ {
path: 'mining/pool/:poolId', path: 'hashrate/pools',
component: PoolComponent, component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
}, },
{ {
path: 'graphs', path: 'graphs',
@ -170,16 +187,31 @@ let routes: Routes = [
component: LatestBlocksComponent, component: LatestBlocksComponent,
}, },
{ {
path: 'mining/difficulty', path: 'mining',
component: DifficultyChartComponent, component: MiningStartComponent,
}, children: [
{ {
path: 'mining/pools', path: 'hashrate',
component: PoolRankingComponent, component: HashrateChartComponent,
}, },
{ {
path: 'mining/pool/:poolId', path: 'hashrate/pools',
component: PoolComponent, component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
}, },
{ {
path: 'graphs', path: 'graphs',
@ -264,16 +296,31 @@ let routes: Routes = [
component: LatestBlocksComponent, component: LatestBlocksComponent,
}, },
{ {
path: 'mining/difficulty', path: 'mining',
component: DifficultyChartComponent, component: MiningStartComponent,
}, children: [
{ {
path: 'mining/pools', path: 'hashrate',
component: PoolRankingComponent, component: HashrateChartComponent,
}, },
{ {
path: 'mining/pool/:poolId', path: 'hashrate/pools',
component: PoolComponent, component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
}, },
{ {
path: 'graphs', path: 'graphs',

View File

@ -71,7 +71,23 @@ export const chartColors = [
"#263238", "#263238",
]; ];
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, export const poolsColor = {
'foundryusa': '#D81B60',
'antpool': '#8E24AA',
'f2pool': '#5E35B1',
'poolin': '#3949AB',
'binancepool': '#1E88E5',
'viabtc': '#039BE5',
'btccom': '#00897B',
'slushpool': '#00ACC1',
'sbicrypto': '#43A047',
'marapool': '#7CB342',
'luxor': '#C0CA33',
'unknown': '#FDD835',
'okkong': '#FFB300',
}
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
export interface Language { export interface Language {

View File

@ -68,8 +68,12 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component'; import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component'; import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -120,8 +124,12 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul
AssetsNavComponent, AssetsNavComponent,
AssetsFeaturedComponent, AssetsFeaturedComponent,
AssetGroupComponent, AssetGroupComponent,
AssetCirculationComponent,
MiningDashboardComponent, MiningDashboardComponent,
DifficultyChartComponent, HashrateChartComponent,
HashrateChartPoolsComponent,
MiningStartComponent,
AmountShortenerPipe,
], ],
imports: [ imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }), BrowserModule.withServerTransition({ appId: 'serverApp' }),

View File

@ -69,7 +69,7 @@ export function calcSegwitFeeGains(tx: Transaction) {
export function moveDec(num: number, n: number) { export function moveDec(num: number, n: number) {
let frac, int, neg, ref; let frac, int, neg, ref;
if (n === 0) { if (n === 0) {
return num; return num.toString();
} }
ref = ('' + num).split('.'), int = ref[0], frac = ref[1]; ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
int || (int = '0'); int || (int = '0');
@ -130,3 +130,32 @@ export const formatNumber = (s, precision = null) => {
// Utilities for segwitFeeGains // Utilities for segwitFeeGains
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0); const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
// Power of ten wrapper
export function selectPowerOfTen(val: number) {
const powerOfTen = {
exa: Math.pow(10, 18),
peta: Math.pow(10, 15),
terra: Math.pow(10, 12),
giga: Math.pow(10, 9),
mega: Math.pow(10, 6),
kilo: Math.pow(10, 3),
};
let selectedPowerOfTen;
if (val < powerOfTen.mega) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (val < powerOfTen.giga) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (val < powerOfTen.terra) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
} else if (val < powerOfTen.peta) {
selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
} else if (val < powerOfTen.exa) {
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
} else {
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
}
return selectedPowerOfTen;
}

View File

@ -8,13 +8,10 @@
</div> </div>
</div> </div>
<div class="about-text" *ngIf="stateService.env.BASE_MODULE === 'mempool'; else marginBox"> <div class="about-text">
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template></h5> <h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> &trade;</ng-template></h5>
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p> <p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
</div> </div>
<ng-template #marginBox>
<div class="no-about-margin"></div>
</ng-template>
<div class="social-icons"> <div class="social-icons">
<a target="_blank" href="https://github.com/mempool/mempool"> <a target="_blank" href="https://github.com/mempool/mempool">
@ -31,31 +28,128 @@
</a> </a>
</div> </div>
<br><br>
<div class="sponsor-button">
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
</div>
<div class="enterprise-sponsor"> <div class="enterprise-sponsor">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper"> <div class="wrapper">
<a href="https://spiral.xyz/" target="_blank" title="Spiral"> <a href="https://spiral.xyz/" target="_blank" title="Spiral">
<img class="image" src="/resources/profile/spiral.svg" /> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-115 -15 879 679" style="background-color: rgb(27,20,100)" class="image">
<defs>
<style>.cls-1{fill:url(#linear-gradient);}</style>
<linearGradient id="linear-gradient" x1="81.36" y1="311.35" x2="541.35" y2="311.35" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="blue"/>
<stop offset="1" stop-color="#f0f"/>
</linearGradient>
</defs>
<path class="cls-1" d="M326.4,572.09C201.2,572.09,141,503,112.48,445,84.26,387.47,81.89,330.44,81.69,322.31c-4.85-77,41-231.78,249.58-271.2a28.05,28.05,0,0,1,10.41,55.13c-213.12,40.28-204.44,206-204,213,0,.53.06,1.06.07,1.6C137.9,328.74,142.85,516,326.4,516,394.74,516,443,486.6,470,428.63c24.48-52.74,19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46,0-63.47,9.88-86.21,29.37A91.5,91.5,0,0,0,248,322.3c-1.41,25.4,7.14,49.36,24.07,67.49C287.27,406,305,413.9,326.4,413.9c27.46,0,45.52-9,53.66-26.81,8.38-18.3,3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78,4.61-8.48,10.92a28,28,0,0,1-51.58-22c14.28-33.44,37.94-42,50.76-44.2,24.78-4.18,52.17,7.3,73.34,30.65s25.51,68.55,10.15,103.22C421.54,432,394.52,470,326.4,470c-36.72,0-69.67-14.49-95.29-41.92C203.64,398.68,189.77,360,192,319.19a149.1,149.1,0,0,1,51.31-104.6c33.19-28.45,74.48-42.87,122.71-42.87,55.12,0,101.85,23.25,135.12,67.23,45.36,60,52.9,141.71,19.66,213.3C495.45,506.92,441.12,572.09,326.4,572.09Z"/>
</svg>
<span>Spiral</span> <span>Spiral</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://gemini.com/" target="_blank" title="Gemini">
<img class="image" src="/resources/profile/gemini.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: white" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span> <span>Gemini</span>
</a> </a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <a href="https://exodus.com/" target="_blank" title="Exodus">
<img class="image" src="/resources/profile/exodus.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<defs>
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
<stop stop-color="#00BFFF" offset="0%"></stop>
<stop stop-color="#6619FF" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>
<span>Exodus</span> <span>Exodus</span>
</a> </a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry"> <a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<img class="image" src="/resources/profile/foundry.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
</g>
</g>
</svg>
<span>Foundry</span> <span>Foundry</span>
</a> </a>
<a href="https://unchained.com/" target="_blank" title="Unchained"> <a href="https://unchained.com/" target="_blank" title="Unchained">
<img class="image" src="/resources/profile/unchained.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span> <span>Unchained</span>
</a> </a>
<a href="https://blockstream.com/" target="_blank" title="Blockstream"> <a href="https://blockstream.com/" target="_blank" title="Blockstream">
<img class="image" src="/resources/profile/blockstream.svg" /> <svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
<style type="text/css">
.st0{fill:#111316;}
.st1{fill:#00C3FF;}
.st2{fill:#7EE0FF;}
</style>
<path class="st1" d="M659.7,392.3c10.2,14.3,18.4,29.9,24.5,46.4l21.8-7.1c-6.9-18.9-16.4-36.8-28.1-53.1L659.7,392.3z"/>
<path class="st1" d="M510.6,289.2c-5.8-0.2-11.7-0.2-17.5,0l1.6,22.8c8.8-0.3,17.6-0.1,26.3,0.7c8.7,0.8,17.4,2.2,26,4.2l5.8-22.1 c-9.8-2.3-19.7-3.9-29.7-4.8C519,289.6,514.7,289.3,510.6,289.2z"/>
<path class="st1" d="M297.1,605.5c-9.1-18.6-15.7-38.3-19.5-58.6l-23.9,3.8c4.2,23,11.6,45.3,22,66.2L297.1,605.5z"/>
<path class="st1" d="M284.8,375.6l21.2,11.8c10.6-17.8,23.5-34,38.5-48.3l-16.2-18C311.3,337.2,296.7,355.5,284.8,375.6z"/>
<path class="st1" d="M254.8,453.5l23.8,4.2c4.2-20.3,11.2-39.9,20.7-58.3l-21.2-11.7C267.3,408.5,259.5,430.6,254.8,453.5z"/>
<path class="st1" d="M409.9,268.8l9.5,22.2c19.3-7.6,39.5-12.5,60.1-14.5l-1.7-24.1C454.5,254.6,431.7,260.1,409.9,268.8z"/>
<path class="st1" d="M338.5,311.8l16.2,18c15.8-13.4,33.3-24.6,52.1-33.4l-9.5-22.2C376,283.9,356.2,296.6,338.5,311.8z"/>
<path class="st1" d="M697.1,667.6l-18.9-15.1c-13.4,15.8-28.9,29.7-46,41.4l13,20.5C664.6,701.3,682.1,685.6,697.1,667.6z"/>
<path class="st1" d="M402.5,710.7c-18.6-9.1-35.9-20.7-51.4-34.5l-16.5,17.7c17.4,15.6,37,28.6,58,38.8L402.5,710.7z"/>
<path class="st1" d="M755.4,528.2c3.1-32.6-0.2-65.5-9.7-96.8l-23,7.6c13.2,44.4,12.7,91.7-1.3,135.8l22.8,8.1 C749.9,565.2,753.7,546.8,755.4,528.2z"/>
<path class="st1" d="M614.2,689.2L602,670c-15.1,9-31.3,16-48.3,20.7l5.4,22.2C578.5,707.5,597,699.6,614.2,689.2z"/>
<path class="st1" d="M314.5,528.8c-1.7-14.2-1.9-28.6-0.5-42.9c0.3-3.5,0.7-6.5,1.2-9.6l-22.5-4c-0.5,3.8-1,7.6-1.4,11.5 c-1.5,16.1-1.3,32.4,0.7,48.5L314.5,528.8z"/>
<path class="st1" d="M568.2,284.7c19.9,5.8,38.9,14.4,56.4,25.4l13.5-20.2c-19.8-12.5-41.2-22.1-63.7-28.7L568.2,284.7z"/>
<path class="st1" d="M469.8,755.8l2.3-24.1c-19.5-2.6-38.6-7.8-56.8-15.3l-10.1,22.2C425.8,747.1,447.6,752.9,469.8,755.8z"/>
<path class="st1" d="M351.3,657.7l15.7-16.6c-12.4-12.5-23.1-26.5-31.8-41.8l-20.3,10.7C324.8,627.4,337.1,643.5,351.3,657.7z"/>
<path class="st1" d="M649.5,297.7l-13.6,20.2c16.9,12,32,26.3,45.1,42.4l19.4-14.8C685.7,327.2,668.6,311.2,649.5,297.7z"/>
<path class="st1" d="M672.7,633.2c12-16.1,21.8-33.7,29.1-52.5l-21.5-7.7c-6.4,16.4-15,31.9-25.5,46L672.7,633.2z"/>
<path class="st2" d="M690.6,449.6l-21.6,7.2c6,20.7,8,42.4,6,63.8c-1.1,11.9-3.4,23.7-6.9,35.2l21.5,7.6c4.1-13.2,6.9-26.9,8.2-40.7 C700.1,498.1,697.6,473.3,690.6,449.6z"/>
<path class="st2" d="M475.2,698l2.1-22.7c-13.3-2-26.4-5.5-38.9-10.5l-9.4,20.7C443.8,691.5,459.3,695.7,475.2,698z"/>
<path class="st2" d="M631.8,456.2l20.4-6.9c-4.9-12.9-11.4-25.2-19.4-36.6l-17.1,13C622.3,435.2,627.7,445.4,631.8,456.2z"/>
<path class="st2" d="M508.4,345.7h-11.2l1.5,21.4c11.5-0.3,22.9,0.7,34.2,3.2l5.5-20.7c-6.8-1.5-13.6-2.6-20.5-3.2 C514.8,346.1,511.6,345.9,508.4,345.7z"/>
<path class="st2" d="M335.5,403.8l20,11.1c7.5-12.4,16.5-23.7,26.9-33.8L367,364.1C354.8,375.9,344.2,389.2,335.5,403.8z"/>
<path class="st2" d="M553.8,339.5c13.8,4.2,27.1,10.2,39.4,17.7l12.7-19c-14.4-8.9-30-15.8-46.2-20.7L553.8,339.5z"/>
<path class="st2" d="M635.9,394.5l18.1-13.8c-10.7-13.2-23.2-24.9-36.9-34.8l-12.7,19C616.2,373.4,626.7,383.3,635.9,394.5z"/>
<path class="st2" d="M611.5,584.6l16.8,13.4c8.2-11.2,14.9-23.3,20.1-36.2l-20.2-7.2C623.8,565.2,618.2,575.3,611.5,584.6z"/>
<path class="st2" d="M389.9,635.1l-15.6,16.6c12.8,11.2,26.9,20.7,42.2,28.2l9.4-20.7C412.9,652.8,400.8,644.6,389.9,635.1z"/>
<path class="st2" d="M369.2,520.2c-1-9.7-1.1-19.5-0.2-29.2c0.2-1.7,0.4-3.5,0.6-5.1l-21.1-3.8c-0.3,2.3-0.6,4.6-0.8,6.9 c-1.1,11.5-0.9,23,0.3,34.5L369.2,520.2z"/>
<path class="st2" d="M333.6,538l-22.6,3.5c3.2,16.7,8.6,33,16,48.3l20.2-10.7C340.9,566,336.4,552.2,333.6,538z"/>
<path class="st2" d="M601.7,646.3l12.3,19.2c14-9.6,26.7-21,37.7-33.8l-17.9-14.2C624.4,628.4,613.6,638.1,601.7,646.3z"/>
<path class="st2" d="M348.8,426.9l-19.9-11c-7.8,15.1-13.5,31.2-17,47.8l22.5,4C337.4,453.5,342.2,439.8,348.8,426.9z"/>
<path class="st2" d="M540.6,636.9l5,20.7c13.3-3.8,26.1-9.2,38.1-16.2l-11.6-18.1C562.2,629,551.6,633.6,540.6,636.9z"/>
<path class="st2" d="M384,573.5l-19,9.9c6.9,12,15.4,23,25.1,32.9l14.8-15.7C396.9,592.4,389.9,583.3,384,573.5z"/>
<path class="st2" d="M496.7,677.1c-1.9,0-3.8-0.2-5.7-0.4l-2.1,22.7c17.9,1.3,35.9,0.1,53.4-3.5l-5.3-22.2 C523.8,676.5,510.2,677.6,496.7,677.1z"/>
<path class="st2" d="M377.3,354.9l15.3,16.9c11.1-9.3,23.3-17.1,36.4-23.3l-9-21C404.6,334.7,390.3,343.9,377.3,354.9z"/>
<path class="st2" d="M432.7,322.1l9,21c13.5-5.2,27.6-8.7,42-10.3L482,310C465.1,311.9,448.5,315.9,432.7,322.1z"/>
<path class="st1" d="M490.3,757.5c21.5,0.7,43-1.1,64.2-5.2l-5-23.3c-18.3,3.8-37,5.3-55.8,4.6c-3,0-5.2-0.4-8.2-0.6l-2.1,24.4 c2.3,0.1,4.6,0.1,6.9,0L490.3,757.5z"/>
</svg>
<span>Blockstream</span> <span>Blockstream</span>
</a> </a>
</div> </div>
@ -73,9 +167,6 @@
</ng-template> </ng-template>
</ng-container> </ng-container>
</div> </div>
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
</div> </div>
<div class="community-integrations-sponsor"> <div class="community-integrations-sponsor">
@ -106,6 +197,10 @@
<img class="image" src="/resources/profile/runcitadel.svg" /> <img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span> <span>Citadel</span>
</a> </a>
<a href="https://github.com/fort-nix/nix-bitcoin" target="_blank" title="nix-bitcoin">
<img class="image" src="/resources/profile/nix-bitcoin.png" />
<span>NixOS</span>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet"> <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
<img class="image" src="/resources/profile/electrum.jpg" /> <img class="image" src="/resources/profile/electrum.jpg" />
<span>Electrum</span> <span>Electrum</span>
@ -142,10 +237,6 @@
<img class="image" src="/resources/profile/marina.svg" /> <img class="image" src="/resources/profile/marina.svg" />
<span>Marina</span> <span>Marina</span>
</a> </a>
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
<img class="image" src="/resources/profile/satpile.jpg" />
<span>Satpile</span>
</a>
</div> </div>
</div> </div>
@ -177,7 +268,7 @@
</div> </div>
<br> <br>
</ng-container> </ng-container>
<ng-container *ngIf="allContributors$ | async as contributors else loadingSponsors"> <ng-container *ngIf="allContributors$ | async as contributors else loadingSponsors">
<div class="contributors"> <div class="contributors">
<h3 i18n="about.contributors">Project Contributors</h3> <h3 i18n="about.contributors">Project Contributors</h3>
@ -252,7 +343,7 @@
<a href="/3rdpartylicenses.txt">Third-party Licenses</a> <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a> <a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
</div> </div>
<div class="footer-version" *ngIf="officialMempoolSpace"> <div class="footer-version" *ngIf="officialMempoolSpace">
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>] {{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
</div> </div>

View File

@ -9,7 +9,7 @@
border-radius: 50%; border-radius: 50%;
margin: 25px; margin: 25px;
line-height: 32px; line-height: 32px;
} }
.intro { .intro {
margin: 25px auto 30px; margin: 25px auto 30px;
@ -41,7 +41,7 @@
} }
.alliances, .alliances,
.enterprise-sponsor, .enterprise-sponsor,
.community-integrations-sponsor, .community-integrations-sponsor,
.maintainers { .maintainers {
margin-top: 68px; margin-top: 68px;
@ -58,7 +58,7 @@
.wrapper { .wrapper {
margin: 20px auto; margin: 20px auto;
} }
.btn-primary { .btn-primary {
max-width: 250px; max-width: 250px;
margin: auto; margin: auto;
height: 45px; height: 45px;
@ -68,7 +68,7 @@
.alliances { .alliances {
margin-bottom: 100px; margin-bottom: 100px;
a { a {
&:nth-child(3) { &:nth-child(3) {
position: relative; position: relative;
top: 10px; top: 10px;
@ -88,17 +88,17 @@
margin: 50px 30px 0px; margin: 50px 30px 0px;
} }
} }
.liquid { .liquid {
top: 7px; top: 7px;
position: relative; position: relative;
} }
.copa { .copa {
height: auto; height: auto;
top: 23px; top: 23px;
position: relative; position: relative;
width: 300px; width: 300px;
} }
.bisq { .bisq {
top: 3px; top: 3px;
position: relative; position: relative;
} }
@ -115,15 +115,15 @@
display: inline-block; display: inline-block;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
img { img, svg {
transform: scale(1.1); transform: scale(1.1);
} }
} }
img, span{ img, svg, span {
display: block; display: block;
transition: 150ms all; transition: 150ms all;
} }
img { img, svg {
margin: 40px 29px 10px; margin: 40px 29px 10px;
} }
} }

View File

@ -46,53 +46,68 @@ export class AddressLabelsComponent implements OnInit {
return; return;
} }
[ // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
// {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'}, if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSHBYTES_(1 \w{2}|2 \w{4}) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
{regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'} if (this.vin.witness[this.vin.witness.length - 2] == '01') {
].forEach((item) => { this.lightning = 'Revoked Force Close';
if (item.regexp.test(this.vin.inner_witnessscript_asm)) { } else {
this.lightning = item.label; this.lightning = 'Force Close';
} }
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
} else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
if (this.vin.witness[this.vin.witness.length - 2].length == 66) {
this.lightning = 'Revoked HTLC';
} else {
this.lightning = 'HTLC';
} }
); }
if (this.lightning) { if (this.lightning) {
return; return;
} }
if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) { this.detectMultisig(this.vin.inner_witnessscript_asm);
const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1); }
this.multisig = true;
this.multisigM = parseInt(matches[0], 10);
this.multisigN = parseInt(matches[1], 10);
if (this.multisigM === 1 && this.multisigN === 1) { this.detectMultisig(this.vin.inner_redeemscript_asm);
this.multisig = false; }
}
detectMultisig(script: string) {
if (!script) {
return;
}
const ops = script.split(' ');
if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') {
return;
}
const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0]);
if (ops.length < n * 2 + 1) {
return;
}
// pop n public keys
for (var i = 0; i < n; i++) {
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
return;
}
if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
return;
} }
} }
const opM = ops.pop();
if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) { if (!opM.startsWith('OP_PUSHNUM_')) {
const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1); return;
this.multisig = true;
this.multisigM = matches[0];
this.multisigN = matches[1];
} }
const m = parseInt(opM.match(/[0-9]+/)[0]);
this.multisig = true;
this.multisigM = m;
this.multisigN = n;
} }
handleVout() { handleVout() {
} }
getMatches(str: string, regex: RegExp, index: number) {
if (!index) {
index = 1;
}
const matches = [];
let match;
while (match = regex.exec(str)) {
matches.push(match[index]);
}
return matches;
}
} }

View File

@ -0,0 +1,6 @@
<ng-container *ngIf="(circulatingAmount$ | async) as circulating">
<ng-template [ngIf]="circulating.amount === -1" [ngIfElse]="default" i18n="shared.confidential">Confidential</ng-template>
<ng-template #default>
<span class="d-inline-block d-md-none">{{ circulating.amount | amountShortener }}</span>
<span class="d-none d-md-inline-block">{{ circulating.amount | number: '1.2-2' }}</span>&nbsp;<span class="ticker">{{ circulating.ticker }}</span></ng-template>
</ng-container>

View File

@ -0,0 +1,3 @@
.ticker {
color: grey;
}

View File

@ -0,0 +1,60 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { moveDec } from 'src/app/bitcoin.utils';
import { AssetsService } from 'src/app/services/assets.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { formatNumber } from '@angular/common';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-asset-circulation',
templateUrl: './asset-circulation.component.html',
styleUrls: ['./asset-circulation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AssetCirculationComponent implements OnInit {
@Input() assetId: string;
circulatingAmount$: Observable<{ amount: number, ticker: string}>;
constructor(
private electrsApiService: ElectrsApiService,
private assetsService: AssetsService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
this.circulatingAmount$ = combineLatest([
this.electrsApiService.getAsset$(this.assetId),
this.assetsService.getAssetsMinimalJson$]
)
.pipe(
map(([asset, assetsMinimal]) => {
const assetData = assetsMinimal[asset.asset_id];
if (!asset.chain_stats.has_blinded_issuances) {
if (asset.asset_id === environment.nativeAssetId) {
return {
amount: this.formatAmount(asset.chain_stats.peg_in_amount - asset.chain_stats.burned_amount - asset.chain_stats.peg_out_amount, assetData[3]),
ticker: assetData[1]
};
} else {
return {
amount: this.formatAmount(asset.chain_stats.issued_amount - asset.chain_stats.burned_amount, assetData[3]),
ticker: assetData[1]
};
}
} else {
return {
amount: -1,
ticker: '',
};
}
}),
);
}
formatAmount(value: number, precision = 0): number {
return parseFloat(moveDec(value, -precision));
}
}

View File

@ -13,7 +13,10 @@
<div class="fee-span"> <div class="fee-span">
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
</div> </div>
<div class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div> <div *ngIf="showMiningInfo" class="block-size">
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<div class="transaction-count"> <div class="transaction-count">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> <ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
@ -21,10 +24,10 @@
</div> </div>
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> <div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
</div> </div>
<div class="" *ngIf="showMiningInfo === true"> <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]"> <a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
{{ block.extras.pool.name}}</a> {{ block.extras.pool.name}}</a>
</div> </div>
</div> </div>
</div> </div>
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div> <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>

View File

@ -129,4 +129,15 @@
position: relative; position: relative;
top: 15px; top: 15px;
z-index: 101; z-index: 101;
} }
.animated {
transition: all 0.15s ease-in-out;
}
.show {
opacity: 1;
}
.hide {
opacity: 0;
pointer-events : none;
}

View File

@ -1,9 +1,10 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { Router } from '@angular/router';
import { specialBlocks } from 'src/app/app.constants'; import { specialBlocks } from 'src/app/app.constants';
import { BlockExtended } from 'src/app/interfaces/node-api.interface'; import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { Location } from '@angular/common';
import { config } from 'process';
@Component({ @Component({
selector: 'app-blockchain-blocks', selector: 'app-blockchain-blocks',
@ -12,7 +13,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class BlockchainBlocksComponent implements OnInit, OnDestroy { export class BlockchainBlocksComponent implements OnInit, OnDestroy {
@Input() showMiningInfo: boolean = false;
specialBlocks = specialBlocks; specialBlocks = specialBlocks;
network = ''; network = '';
blocks: BlockExtended[] = []; blocks: BlockExtended[] = [];
@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
arrowLeftPx = 30; arrowLeftPx = 30;
blocksFilled = false; blocksFilled = false;
transition = '1s'; transition = '1s';
showMiningInfo = false;
gradientColors = { gradientColors = {
'': ['#9339f4', '#105fb0'], '': ['#9339f4', '#105fb0'],
@ -44,11 +45,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private router: Router,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
) { } private location: Location,
) {
}
enabledMiningInfoIfNeeded(url) {
this.showMiningInfo = url === '/mining';
this.cd.markForCheck(); // Need to update the view asap
}
ngOnInit() { ngOnInit() {
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
}
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
this.feeRounding = '1.0-1'; this.feeRounding = '1.0-1';
} }

View File

@ -1,8 +1,8 @@
<div class="text-center" class="blockchain-wrapper animate" #container> <div class="text-center" class="blockchain-wrapper" #container>
<div class="position-container {{ network }}"> <div class="position-container {{ network }}">
<span> <span>
<app-mempool-blocks></app-mempool-blocks> <app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks [showMiningInfo]="showMiningInfo"></app-blockchain-blocks> <app-blockchain-blocks></app-blockchain-blocks>
<div id="divider"></div> <div id="divider"></div>
</span> </span>
</div> </div>

View File

@ -59,14 +59,4 @@
width: 300px; width: 300px;
left: -150px; left: -150px;
top: 0px; top: 0px;
} }
.animate {
transition: all 1s ease-in-out;
}
.move-left {
transform: translate(-40%, 0);
@media (max-width: 767.98px) {
transform: translate(-85%, 0);
}
}

View File

@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class BlockchainComponent implements OnInit { export class BlockchainComponent implements OnInit {
showMiningInfo: boolean = false;
network: string; network: string;
constructor( constructor(

View File

@ -1,53 +0,0 @@
<div [class]="widget === false ? 'container-xl' : ''">
<div *ngIf="difficultyObservable$ | async" class="" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
<div class="card-header mb-0 mb-lg-4" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(difficultyObservable$ | async) as diffChanges">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 90">
<input ngbButton type="radio" [value]="'3m'" fragment="3m"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 180">
<input ngbButton type="radio" [value]="'6m'" fragment="6m"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 365">
<input ngbButton type="radio" [value]="'1y'" fragment="1y"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 730">
<input ngbButton type="radio" [value]="'2y'" fragment="2y"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" [routerLink]="['/mining/difficulty' | relativeUrl]" *ngIf="diffChanges.availableTimespanDay >= 1095">
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" [routerLink]="['/mining/difficulty' | relativeUrl]" fragment="all"> ALL
</label>
</div>
</form>
</div>
<table class="table table-borderless table-sm text-center" *ngIf="!widget">
<thead>
<tr>
<th i18n="mining.rank">Block</th>
<th i18n="block.timestamp">Timestamp</th>
<th i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change">Change</th>
</tr>
</thead>
<tbody *ngIf="(difficultyObservable$ | async) as diffChanges">
<tr *ngFor="let diffChange of diffChanges.data">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td>&lrm;{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,10 +0,0 @@
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}

View File

@ -1,154 +0,0 @@
import { 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 { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-difficulty-chart',
templateUrl: './difficulty-chart.component.html',
styleUrls: ['./difficulty-chart.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 38%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class DifficultyChartComponent implements OnInit {
@Input() widget: boolean = false;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg'
};
difficultyObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
) {
this.seoService.setTitle($localize`:@@mining.difficulty:Difficulty`);
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
const powerOfTen = {
terra: Math.pow(10, 12),
giga: Math.pow(10, 9),
mega: Math.pow(10, 6),
kilo: Math.pow(10, 3),
}
this.difficultyObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith('1y'),
switchMap((timespan) => {
return this.apiService.getHistoricalDifficulty$(timespan)
.pipe(
tap(data => {
this.prepareChartOptions(data.adjustments.map(val => [val.timestamp * 1000, val.difficulty]));
this.isLoading = false;
}),
map(data => {
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp / 1000)
) / 3600 / 24;
const tableData = [];
for (let i = 0; i < data.adjustments.length - 1; ++i) {
const change = (data.adjustments[i].difficulty / data.adjustments[i + 1].difficulty - 1) * 100;
let selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
if (data.adjustments[i].difficulty < powerOfTen.mega) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (data.adjustments[i].difficulty < powerOfTen.giga) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (data.adjustments[i].difficulty < powerOfTen.terra) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
}
tableData.push(Object.assign(data.adjustments[i], {
change: change,
difficultyShorten: formatNumber(
data.adjustments[i].difficulty / selectedPowerOfTen.divider,
this.locale, '1.2-2') + selectedPowerOfTen.unit
}));
}
return {
availableTimespanDay: availableTimespanDay,
data: tableData
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
title: {
text: this.widget? '' : $localize`:@@mining.difficulty:Difficulty`,
left: 'center',
textStyle: {
color: '#FFF',
},
},
tooltip: {
show: true,
trigger: 'axis',
},
axisPointer: {
type: 'line',
},
xAxis: {
type: 'time',
splitNumber: this.isMobile() ? 5 : 10,
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (val) => {
const diff = val / Math.pow(10, 12); // terra
return diff.toString() + 'T';
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [
{
data: data,
type: 'line',
smooth: false,
lineStyle: {
width: 3,
},
areaStyle: {}
},
],
};
}
isMobile() {
return (window.innerWidth <= 767.98);
}
}

View File

@ -2,15 +2,18 @@
<div class="container-xl"> <div class="container-xl">
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData"> <div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
<div class="col d-none d-sm-block"> <div class="col d-none d-sm-block">
<span class="txPerSecond" i18n="footer.tx-vbytes-per-second">Tx vBytes per second:</span> <span class="txPerSecond" i18n="dashboard.incoming-transactions">Incoming transactions</span>&nbsp;
<span *ngIf="mempoolInfoData.vBytesPerSecond === 0; else inSync"> <ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData" [ngIfElse]="loadingTransactions">
&nbsp;<span class="badge badge-pill badge-warning" i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</span> <span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
</span> &nbsp;<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
<ng-template #inSync> </span>
<div class="progress sub-text"> <ng-template #inSync>
<div class="progress-bar {{ mempoolInfoData.progressClass }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth}">{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div> <div class="progress inc-tx-progress-bar">
</div> <div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}">&nbsp;</div>
</ng-template> <div class="progress-text">&lrm;{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
</div>
</ng-template>
</ng-template>
</div> </div>
<div class="col"> <div class="col">
<span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span> <span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span>
@ -25,3 +28,7 @@
</div> </div>
</div> </div>
</footer> </footer>
<ng-template #loadingTransactions>
<div class="skeleton-loader skeleton-loader-transactions"></div>
</ng-template>

View File

@ -13,7 +13,7 @@ interface MempoolInfoData {
memPoolInfo: MempoolInfo; memPoolInfo: MempoolInfo;
vBytesPerSecond: number; vBytesPerSecond: number;
progressWidth: string; progressWidth: string;
progressClass: string; progressColor: string;
} }
@Component({ @Component({
@ -26,35 +26,62 @@ export class FooterComponent implements OnInit {
mempoolBlocksData$: Observable<MempoolBlocksData>; mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>; mempoolInfoData$: Observable<MempoolInfoData>;
vBytesPerSecondLimit = 1667; vBytesPerSecondLimit = 1667;
isLoadingWebSocket$: Observable<boolean>;
mempoolLoadingStatus$: Observable<number>;
constructor( constructor(
private stateService: StateService, private stateService: StateService,
) { } ) { }
ngOnInit() { ngOnInit() {
this.mempoolInfoData$ = combineLatest([ this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.stateService.mempoolInfo$, this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
this.stateService.vbytesPerSecond$ .pipe(
]) map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
.pipe( );
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressClass = 'bg-danger'; this.mempoolInfoData$ = combineLatest([
if (percent <= 75) { this.stateService.mempoolInfo$,
progressClass = 'bg-success'; this.stateService.vbytesPerSecond$
} else if (percent <= 99) { ])
progressClass = 'bg-warning'; .pipe(
} map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
return {
memPoolInfo: mempoolInfo, let progressColor = '#7CB342';
vBytesPerSecond: vbytesPerSecond, if (vbytesPerSecond > 1667) {
progressWidth: percent + '%', progressColor = '#FDD835';
progressClass: progressClass, }
}; if (vbytesPerSecond > 2000) {
}) progressColor = '#FFB300';
); }
if (vbytesPerSecond > 2500) {
progressColor = '#FB8C00';
}
if (vbytesPerSecond > 3000) {
progressColor = '#F4511E';
}
if (vbytesPerSecond > 3500) {
progressColor = '#D81B60';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
let mempoolSizeProgress = 'bg-danger';
if (mempoolSizePercentage <= 50) {
mempoolSizeProgress = 'bg-success';
} else if (mempoolSizePercentage <= 75) {
mempoolSizeProgress = 'bg-warning';
}
return {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$ this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
.pipe( .pipe(

View File

@ -0,0 +1,58 @@
<div [class]="widget === false ? 'full-container' : ''">
<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">
<input ngbButton type="radio" [value]="'3m'"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
<input ngbButton type="radio" [value]="'6m'"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
<input ngbButton type="radio" [value]="'1y'"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
<input ngbButton type="radio" [value]="'2y'"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
<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>
<div *ngIf="hashrateObservable$ | async" [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="mt-3" *ngIf="!widget">
<table class="table table-borderless table-sm text-center">
<thead>
<tr>
<th i18n="mining.rank">Block</th>
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
<th i18n="mining.adjusted">Adjusted</th>
<th i18n="mining.difficulty">Difficulty</th>
<th i18n="mining.change">Change</th>
</tr>
</thead>
<tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data.difficulty">
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
<td class="d-none d-md-block">&lrm;{{ 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="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
</tr>
</tbody>
</table>
</div> -->
</div>

View File

@ -0,0 +1,50 @@
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
width: 100%;
height: calc(100% - 100px);
@media (max-width: 992px) {
height: calc(100% - 140px);
};
@media (max-width: 576px) {
height: calc(100% - 180px);
};
}
.chart {
width: 100%;
height: 100%;
padding-bottom: 20px;
padding-right: 20px;
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}

View File

@ -0,0 +1,299 @@
import { 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 { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
@Component({
selector: 'app-hashrate-chart',
templateUrl: './hashrate-chart.component.html',
styleUrls: ['./hashrate-chart.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 38%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class HashrateChartComponent implements OnInit {
@Input() widget: boolean = false;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
width: 'auto',
height: 'auto',
};
hashrateObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
}
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith('1y'),
switchMap((timespan) => {
return this.apiService.getHistoricalHashrate$(timespan)
.pipe(
tap((data: any) => {
// We generate duplicated data point so the tooltip works nicely
const diffFixed = [];
let diffIndex = 1;
let hashIndex = 0;
while (hashIndex < data.hashrates.length) {
if (diffIndex >= data.difficulty.length) {
while (hashIndex < data.hashrates.length) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty[data.difficulty.length - 1].difficulty
});
++hashIndex;
}
break;
}
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].timestamp
) {
diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp,
difficulty: data.difficulty[diffIndex - 1].difficulty
});
++hashIndex;
}
++diffIndex;
}
this.prepareChartOptions({
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty])
});
this.isLoading = false;
}),
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
}));
}
return {
availableTimespanDay: availableTimespanDay,
difficulty: tableData
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
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: this.widget ? 30 : 60,
},
tooltip: {
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];
let difficultyPowerOfTen = hashratePowerOfTen;
let difficulty = data[1].data[1];
if (this.isMobile()) {
hashratePowerOfTen = selectPowerOfTen(data[0].data[1]);
hashrate = Math.round(data[0].data[1] / hashratePowerOfTen.divider);
difficultyPowerOfTen = selectPowerOfTen(data[1].data[1]);
difficulty = Math.round(data[1].data[1] / difficultyPowerOfTen.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>
<span>${data[1].marker} ${data[1].seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}</span>
`;
}.bind(this)
},
xAxis: {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
},
legend: {
data: [
{
name: 'Hashrate',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: '#FFB300',
},
},
{
name: 'Difficulty',
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: '#D81B60',
}
},
],
},
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,
}
},
{
min: function (value) {
return value.min * 0.9;
},
type: 'value',
name: 'Difficulty',
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
const selectedPowerOfTen: any = selectPowerOfTen(val);
const newVal = Math.round(val / selectedPowerOfTen.divider);
return `${newVal} ${selectedPowerOfTen.unit}`
}
},
splitLine: {
show: false,
}
}
],
series: [
{
name: 'Hashrate',
showSymbol: false,
symbol: 'none',
data: data.hashrates,
type: 'line',
lineStyle: {
width: 2,
},
},
{
yAxisIndex: 1,
name: 'Difficulty',
showSymbol: false,
symbol: 'none',
data: data.difficulty,
type: 'line',
lineStyle: {
width: 3,
}
}
],
dataZoom: this.widget ? null : [{
type: 'inside',
realtime: true,
zoomLock: true,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
maxSpan: 100,
minSpan: 10,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
bottom: 0,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
},
}],
};
}
isMobile() {
return (window.innerWidth <= 767.98);
}
}

View File

@ -0,0 +1,34 @@
<div [class]="widget === false ? 'full-container' : ''">
<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">
<input ngbButton type="radio" [value]="'3m'"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
<input ngbButton type="radio" [value]="'6m'"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
<input ngbButton type="radio" [value]="'1y'"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
<input ngbButton type="radio" [value]="'2y'"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
<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>
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@ -0,0 +1,60 @@
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
width: 100%;
height: calc(100% - 100px);
@media (max-width: 992px) {
height: calc(100% - 140px);
};
@media (max-width: 576px) {
height: calc(100% - 180px);
};
}
.chart {
width: 100%;
height: 100%;
padding-bottom: 20px;
padding-right: 20px;
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
.loadingGraphs.widget {
top: 75%;
}

View File

@ -0,0 +1,185 @@
import { ChangeDetectionStrategy, 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 { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { poolsColor } from 'src/app/app.constants';
@Component({
selector: 'app-hashrate-chart-pools',
templateUrl: './hashrate-chart-pools.component.html',
styleUrls: ['./hashrate-chart-pools.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HashrateChartPoolsComponent implements OnInit {
@Input() widget: boolean = false;
@Input() right: number | string = 40;
@Input() left: number | string = 25;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
width: 'auto',
height: 'auto',
};
hashrateObservable$: Observable<any>;
isLoading = true;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`);
}
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith('1y'),
switchMap((timespan) => {
this.isLoading = true;
return this.apiService.getHistoricalPoolsHashrate$(timespan)
.pipe(
tap((data: any) => {
// Prepare series (group all hashrates data point by pool)
const grouped = {};
for (const hashrate of data.hashrates) {
if (!grouped.hasOwnProperty(hashrate.poolName)) {
grouped[hashrate.poolName] = [];
}
grouped[hashrate.poolName].push(hashrate);
}
const series = [];
const legends = [];
for (const name in grouped) {
series.push({
stack: 'Total',
name: name,
showSymbol: false,
symbol: 'none',
data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]),
type: 'line',
lineStyle: { width: 0 },
areaStyle: { opacity: 1 },
smooth: true,
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
emphasis: {
disabled: true,
scale: false,
},
});
legends.push({
name: name,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
},
icon: 'roundRect',
itemStyle: {
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
},
});
}
this.prepareChartOptions({
legends: legends,
series: series
});
this.isLoading = false;
}),
map((data: any) => {
const availableTimespanDay = (
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
) / 3600 / 24;
return {
availableTimespanDay: availableTimespanDay,
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
grid: {
right: this.right,
left: this.left,
bottom: this.widget ? 30 : 20,
top: this.widget ? 10 : 40,
},
tooltip: {
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 tooltip = `<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>`;
data.sort((a, b) => b.data[1] - a.data[1]);
for (const pool of data) {
if (pool.data[1] > 0) {
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%<br>`
}
}
return tooltip;
}.bind(this)
},
xAxis: {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
},
legend: (this.isMobile() || this.widget) ? undefined : {
data: data.legends
},
yAxis: {
position: 'right',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => `${val}%`,
},
splitLine: {
show: false,
},
type: 'value',
max: 100,
min: 0,
},
series: data.series,
};
}
isMobile() {
return (window.innerWidth <= 767.98);
}
}

View File

@ -160,6 +160,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
type: 'line', type: 'line',
smooth: false, smooth: false,
showSymbol: false, showSymbol: false,
symbol: 'none',
lineStyle: { lineStyle: {
width: 3, width: 3,
}, },

View File

@ -31,7 +31,7 @@
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a> <a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
</li> </li>
<li class="nav-item" routerLinkActive="active" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a> <a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li> </li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD"> <li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">

View File

@ -12,7 +12,10 @@
<div class="fee-span"> <div class="fee-span">
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</div> </div>
<div class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div> <div *ngIf="showMiningInfo" class="block-size">
<app-amount [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</div>
<div *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'&lrm;' + (projectedBlock.blockSize | bytes: 2)"></div>
<div class="transaction-count"> <div class="transaction-count">
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container> <ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>

View File

@ -7,6 +7,7 @@ import { take, map, switchMap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
import { specialBlocks } from 'src/app/app.constants'; import { specialBlocks } from 'src/app/app.constants';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { Location } from '@angular/common';
@Component({ @Component({
selector: 'app-mempool-blocks', selector: 'app-mempool-blocks',
@ -32,6 +33,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
networkSubscription: Subscription; networkSubscription: Subscription;
network = ''; network = '';
now = new Date().getTime(); now = new Date().getTime();
showMiningInfo = false;
blockWidth = 125; blockWidth = 125;
blockPadding = 30; blockPadding = 30;
@ -54,9 +56,20 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
public stateService: StateService, public stateService: StateService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private location: Location
) { } ) { }
enabledMiningInfoIfNeeded(url) {
this.showMiningInfo = url === '/mining';
this.cd.markForCheck(); // Need to update the view asap
}
ngOnInit() { ngOnInit() {
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
this.enabledMiningInfoIfNeeded(this.location.path());
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
}
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
this.feeRounding = '1.0-1'; this.feeRounding = '1.0-1';
} }

View File

@ -1,30 +1,37 @@
<div class="container-xl dashboard-container"> <div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2"> <div class="row row-cols-1 row-cols-md-2">
<!-- pool distribution -->
<div class="col"> <div class="col">
<div class="main-title" i18n="mining.pool-share">Mining Pools Share (1w)</div> <div class="card double">
<div class="card">
<div class="card-body"> <div class="card-body">
<!-- pool distribution -->
<h5 class="card-title">
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
Mining Pools Share (1w)
</a>
</h5>
<app-pool-ranking [widget]=true></app-pool-ranking> <app-pool-ranking [widget]=true></app-pool-ranking>
<div class="text-center"><a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div> <!-- pools hashrate -->
<app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
</div> </div>
</div> </div>
</div> </div>
<!-- difficulty -->
<div class="col"> <div class="col">
<div class="main-title" i18n="mining.difficulty">Difficulty (1y)</div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<app-difficulty-chart [widget]=true></app-difficulty-chart> <!-- hashrate -->
<div class="text-center"><a href="" [routerLink]="['/mining/difficulty' | relativeUrl]" i18n="dashboard.view-more">View more <h5 class="card-title">
&raquo;</a></div> <a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
Hashrate (1y)
</a>
</h5>
<app-hashrate-chart [widget]=true></app-hashrate-chart>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,21 +12,24 @@
.card { .card {
background-color: #1d1f31; background-color: #1d1f31;
height: 100%; height: 340px;
}
.card.double {
height: 620px;
} }
.card-wrapper { .card-title {
.card { font-size: 1rem;
height: auto !important; }
} .card-title > a {
.card-body { color: #4a68b9;
display: flex; }
flex: inherit;
text-align: center; .card-body {
flex-direction: column; padding: 1.25rem 1rem 0.75rem 1rem;
justify-content: space-around; }
padding: 22px 20px; .card-body.pool-ranking {
} padding: 1.25rem 0.25rem 0.75rem 0.25rem;
} }
#blockchain-container { #blockchain-container {

View File

@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
@Component({ @Component({
selector: 'app-mining-dashboard', selector: 'app-mining-dashboard',
@ -8,7 +9,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
}) })
export class MiningDashboardComponent implements OnInit { export class MiningDashboardComponent implements OnInit {
constructor() { } constructor(private seoService: SeoService) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
}
ngOnInit(): void { ngOnInit(): void {
} }

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -0,0 +1,14 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-mining-start',
templateUrl: './mining-start.component.html',
})
export class MiningStartComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,11 +1,12 @@
<div [class]="widget === false ? 'container-xl' : ''"> <div [class]="widget === false ? 'container-xl' : ''">
<div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div> <div [class]="widget ? 'chart-widget' : 'chart'"
<div class="text-center loadingGraphs" *ngIf="isLoading"> echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="card-header mb-0 mb-lg-4" [style]="widget === true ? 'display:none' : ''"> <div class="card-header mb-0 mb-lg-4 mt-md-3" [style]="widget ? 'display:none' : ''">
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">

View File

@ -1,10 +1,14 @@
.hashrate-pie { .chart {
height: 100%; max-height: 400px;
min-height: 400px;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
min-height: 300px; max-height: 300px;
} }
} }
.chart-widget {
width: 100%;
height: 100%;
max-height: 275px;
}
.formRadioGroup { .formRadioGroup {
margin-top: 6px; margin-top: 6px;
@ -30,3 +34,13 @@
padding: .3em !important; padding: .3em !important;
} }
} }
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
.loadingGraphs.widget {
top: 25%;
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts'; import { EChartsOption, PieSeriesOption } from 'echarts';
@ -9,20 +9,13 @@ import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from '../..//services/storage.service'; import { StorageService } from '../..//services/storage.service';
import { MiningService, MiningStats } from '../../services/mining.service'; import { MiningService, MiningStats } from '../../services/mining.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { chartColors } from 'src/app/app.constants'; import { chartColors, poolsColor } from 'src/app/app.constants';
@Component({ @Component({
selector: 'app-pool-ranking', selector: 'app-pool-ranking',
templateUrl: './pool-ranking.component.html', templateUrl: './pool-ranking.component.html',
styleUrls: ['./pool-ranking.component.scss'], styleUrls: ['./pool-ranking.component.scss'],
styles: [` changeDetection: ChangeDetectionStrategy.OnPush,
.loadingGraphs {
position: absolute;
top: 38%;
left: calc(50% - 15px);
z-index: 100;
}
`],
}) })
export class PoolRankingComponent implements OnInit { export class PoolRankingComponent implements OnInit {
@Input() widget: boolean = false; @Input() widget: boolean = false;
@ -33,7 +26,9 @@ export class PoolRankingComponent implements OnInit {
isLoading = true; isLoading = true;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
renderer: 'svg' renderer: 'svg',
width: 'auto',
height: 'auto',
}; };
chartInstance: any = undefined; chartInstance: any = undefined;
@ -47,13 +42,13 @@ export class PoolRankingComponent implements OnInit {
private seoService: SeoService, private seoService: SeoService,
private router: Router, private router: Router,
) { ) {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
} }
ngOnInit(): void { ngOnInit(): void {
if (this.widget) { if (this.widget) {
this.poolsWindowPreference = '1w'; this.poolsWindowPreference = '1w';
} else { } else {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
} }
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
@ -118,24 +113,30 @@ export class PoolRankingComponent implements OnInit {
return; return;
} }
data.push({ data.push({
itemStyle: {
color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
},
value: pool.share, value: pool.share,
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
label: { label: {
color: '#FFFFFF', color: '#b1b1b1',
overflow: 'break', overflow: 'break',
}, },
tooltip: { tooltip: {
backgroundColor: '#282d47', backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: { textStyle: {
color: '#FFFFFF', color: '#b1b1b1',
}, },
borderColor: '#000',
formatter: () => { formatter: () => {
if (this.poolsWindowPreference === '24h') { if (this.poolsWindowPreference === '24h') {
return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` + return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' + pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + pool.blockCount.toString() + ` blocks`; `<br>` + pool.blockCount.toString() + ` blocks`;
} else { } else {
return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` + return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.blockCount.toString() + ` blocks`; pool.blockCount.toString() + ` blocks`;
} }
} }
@ -153,7 +154,13 @@ export class PoolRankingComponent implements OnInit {
} }
network = network.charAt(0).toUpperCase() + network.slice(1); network = network.charAt(0).toUpperCase() + network.slice(1);
let radius: any[] = ['20%', '70%'];
if (this.isMobile() || this.widget) {
radius = ['20%', '60%'];
}
this.chartOptions = { this.chartOptions = {
color: chartColors,
title: { title: {
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`, text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
left: 'center', left: 'center',
@ -166,17 +173,21 @@ export class PoolRankingComponent implements OnInit {
} }
}, },
tooltip: { tooltip: {
trigger: 'item' trigger: 'item',
textStyle: {
align: 'left',
}
}, },
series: [ series: [
{ {
top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'), top: this.widget ? 0 : 35,
bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
name: 'Mining pool', name: 'Mining pool',
type: 'pie', type: 'pie',
radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']), radius: radius,
data: this.generatePoolsChartSerieData(miningStats), data: this.generatePoolsChartSerieData(miningStats),
labelLine: { labelLine: {
length: this.isMobile() ? 10 : 15,
length2: this.isMobile() ? 0 : 15,
lineStyle: { lineStyle: {
width: 2, width: 2,
}, },
@ -202,7 +213,6 @@ export class PoolRankingComponent implements OnInit {
} }
} }
], ],
color: chartColors
}; };
} }

View File

@ -172,9 +172,12 @@
</td> </td>
<td class="text-right nowrap amount"> <td class="text-right nowrap amount">
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput"> <ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset]"> <div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container> <ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
</div> </div>
<ng-template #assetNotFound>
{{ vout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vout.asset]">{{ vout.asset | slice : 0 : 7 }}</a>
</ng-template>
</ng-template> </ng-template>
<ng-template #defaultOutput> <ng-template #defaultOutput>
<app-amount [satoshis]="vout.value"></app-amount> <app-amount [satoshis]="vout.value"></app-amount>
@ -254,7 +257,7 @@
&nbsp; &nbsp;
</span> </span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()"> <button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template> <ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
<ng-template #defaultAmount> <ng-template #defaultAmount>
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount> <app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
</ng-template> </ng-template>

View File

@ -95,6 +95,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
} }
} }
haveBlindedOutputValues(tx: Transaction): boolean {
return tx.vout.some((v: any) => v.value === undefined);
}
getTotalTxOutput(tx: Transaction) { getTotalTxOutput(tx: Transaction) {
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b); return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
} }

View File

@ -16,58 +16,79 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
</div> </div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
<ng-template #expanded> <ng-template #expanded>
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'"> <ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div> <div class="col card-wrapper">
<div class="card"> <div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card-body"> <div class="card">
<app-fees-box class="d-block"></app-fees-box> <div class="card-body">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div> </div>
</div> </div>
</div> <div class="col">
<div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'"> <app-difficulty></app-difficulty>
<app-difficulty></app-difficulty> </div>
</div> </ng-container>
<div class="col"> <div class="col">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body pl-0"> <div class="card-body pl-0">
<div style="padding-left: 1.25rem;"> <div style="padding-left: 1.25rem;">
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<hr> <hr>
</div> </div>
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats"> <ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
<div class="mempool-graph"> <ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
<app-mempool-graph <div class="mempool-graph">
[template]="'widget'" <app-mempool-graph
[limitFee]="150" [template]="'widget'"
[limitFilterFee]="1" [limitFee]="150"
[data]="mempoolStats.value?.mempool" [limitFilterFee]="1"
[windowPreferenceOverride]="'2h'" [data]="mempoolStats.value?.mempool"
></app-mempool-graph> [windowPreferenceOverride]="'2h'"
</div> ></app-mempool-graph>
</ng-container> </div>
</ng-container>
</ng-template>
<ng-template #liquidPegs>
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
</ng-template>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="card graph-card"> <div class="card graph-card">
<div class="card-body"> <div class="card-body">
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container> <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
<hr> <hr>
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph"> <div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph> <table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
<tbody>
<tr *ngFor="let group of featuredAssets">
<td class="asset-icon">
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
</a>
</td>
<td class="asset-title">
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
</td>
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
</tr>
</tbody>
</table>
</div> </div>
<ng-template #mempoolGraph> <ng-template #mempoolGraph>
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats"> <div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
@ -158,6 +179,27 @@
</div> </div>
<ng-template #loadingAssetsTable>
<table class="table table-borderless table-striped asset-table">
<tbody>
<tr *ngFor="let i of [1,2,3,4]">
<td class="asset-icon">
<div class="skeleton-loader skeleton-loader-transactions"></div>
</td>
<td class="asset-title">
<div class="skeleton-loader skeleton-loader-transactions"></div>
</td>
<td class="asset-title d-none d-md-table-cell">
<div class="skeleton-loader skeleton-loader-transactions"></div>
</td>
<td class="asset-title">
<div class="skeleton-loader skeleton-loader-transactions"></div>
</td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template #loadingTransactions> <ng-template #loadingTransactions>
<div class="skeleton-loader skeleton-loader-transactions"></div> <div class="skeleton-loader skeleton-loader-transactions"></div>
</ng-template> </ng-template>

View File

@ -283,3 +283,25 @@
margin-right: -2px; margin-right: -2px;
font-size: 10px; font-size: 10px;
} }
.assetIcon {
width: 40px;
height: 40px;
}
.asset-title {
text-align: left;
vertical-align: middle;
}
.asset-icon {
width: 65px;
height: 65px;
vertical-align: middle;
}
.circulating-amount {
text-align: right;
width: 100%;
vertical-align: middle;
}

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, timer } from 'rxjs'; import { combineLatest, merge, Observable, of } from 'rxjs';
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
@ -34,6 +34,7 @@ interface MempoolStatsData {
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
collapseLevel: string; collapseLevel: string;
featuredAssets$: Observable<any>;
network$: Observable<string>; network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>; mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>; mempoolInfoData$: Observable<MempoolInfoData>;
@ -124,6 +125,19 @@ export class DashboardComponent implements OnInit {
}) })
); );
this.featuredAssets$ = this.apiService.listFeaturedAssets$()
.pipe(
map((featured) => {
const newArray = [];
for (const feature of featured) {
if (feature.ticker !== 'L-BTC' && feature.asset) {
newArray.push(feature);
}
}
return newArray.slice(0, 4);
}),
);
this.blocks$ = this.stateService.blocks$ this.blocks$ = this.stateService.blocks$
.pipe( .pipe(
tap(([block]) => { tap(([block]) => {

View File

@ -101,6 +101,7 @@ export interface PoolStat {
} }
export interface BlockExtension { export interface BlockExtension {
totalFees?: number;
medianFee?: number; medianFee?: number;
feeRange?: number[]; feeRange?: number[];
reward?: number; reward?: number;

View File

@ -156,4 +156,18 @@ export class ApiService {
(interval !== undefined ? `/${interval}` : '') (interval !== undefined ? `/${interval}` : '')
); );
} }
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
(interval !== undefined ? `/${interval}` : '')
);
}
getHistoricalPoolsHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate/pools` +
(interval !== undefined ? `/${interval}` : '')
);
}
} }

View File

@ -82,7 +82,7 @@ export class MiningService {
}); });
const availableTimespanDay = ( const availableTimespanDay = (
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp / 1000) (new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp)
) / 3600 / 24; ) / 3600 / 24;
return { return {

View File

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'amountShortener'
})
export class AmountShortenerPipe implements PipeTransform {
transform(num: number, ...args: number[]): unknown {
const digits = args[0] || 1;
const lookup = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
{ value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'G' },
{ value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' }
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
var item = lookup.slice().reverse().find((item) => num >= item.value);
return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0';
}
}

View File

@ -1,43 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" id="Layer_1" x="0px" y="0px" viewBox="200 200 600 600" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important" xml:space="preserve">
<style type="text/css">
.st0{fill:#111316;}
.st1{fill:#00C3FF;}
.st2{fill:#7EE0FF;}
</style>
<path class="st1" d="M659.7,392.3c10.2,14.3,18.4,29.9,24.5,46.4l21.8-7.1c-6.9-18.9-16.4-36.8-28.1-53.1L659.7,392.3z"/>
<path class="st1" d="M510.6,289.2c-5.8-0.2-11.7-0.2-17.5,0l1.6,22.8c8.8-0.3,17.6-0.1,26.3,0.7c8.7,0.8,17.4,2.2,26,4.2l5.8-22.1 c-9.8-2.3-19.7-3.9-29.7-4.8C519,289.6,514.7,289.3,510.6,289.2z"/>
<path class="st1" d="M297.1,605.5c-9.1-18.6-15.7-38.3-19.5-58.6l-23.9,3.8c4.2,23,11.6,45.3,22,66.2L297.1,605.5z"/>
<path class="st1" d="M284.8,375.6l21.2,11.8c10.6-17.8,23.5-34,38.5-48.3l-16.2-18C311.3,337.2,296.7,355.5,284.8,375.6z"/>
<path class="st1" d="M254.8,453.5l23.8,4.2c4.2-20.3,11.2-39.9,20.7-58.3l-21.2-11.7C267.3,408.5,259.5,430.6,254.8,453.5z"/>
<path class="st1" d="M409.9,268.8l9.5,22.2c19.3-7.6,39.5-12.5,60.1-14.5l-1.7-24.1C454.5,254.6,431.7,260.1,409.9,268.8z"/>
<path class="st1" d="M338.5,311.8l16.2,18c15.8-13.4,33.3-24.6,52.1-33.4l-9.5-22.2C376,283.9,356.2,296.6,338.5,311.8z"/>
<path class="st1" d="M697.1,667.6l-18.9-15.1c-13.4,15.8-28.9,29.7-46,41.4l13,20.5C664.6,701.3,682.1,685.6,697.1,667.6z"/>
<path class="st1" d="M402.5,710.7c-18.6-9.1-35.9-20.7-51.4-34.5l-16.5,17.7c17.4,15.6,37,28.6,58,38.8L402.5,710.7z"/>
<path class="st1" d="M755.4,528.2c3.1-32.6-0.2-65.5-9.7-96.8l-23,7.6c13.2,44.4,12.7,91.7-1.3,135.8l22.8,8.1 C749.9,565.2,753.7,546.8,755.4,528.2z"/>
<path class="st1" d="M614.2,689.2L602,670c-15.1,9-31.3,16-48.3,20.7l5.4,22.2C578.5,707.5,597,699.6,614.2,689.2z"/>
<path class="st1" d="M314.5,528.8c-1.7-14.2-1.9-28.6-0.5-42.9c0.3-3.5,0.7-6.5,1.2-9.6l-22.5-4c-0.5,3.8-1,7.6-1.4,11.5 c-1.5,16.1-1.3,32.4,0.7,48.5L314.5,528.8z"/>
<path class="st1" d="M568.2,284.7c19.9,5.8,38.9,14.4,56.4,25.4l13.5-20.2c-19.8-12.5-41.2-22.1-63.7-28.7L568.2,284.7z"/>
<path class="st1" d="M469.8,755.8l2.3-24.1c-19.5-2.6-38.6-7.8-56.8-15.3l-10.1,22.2C425.8,747.1,447.6,752.9,469.8,755.8z"/>
<path class="st1" d="M351.3,657.7l15.7-16.6c-12.4-12.5-23.1-26.5-31.8-41.8l-20.3,10.7C324.8,627.4,337.1,643.5,351.3,657.7z"/>
<path class="st1" d="M649.5,297.7l-13.6,20.2c16.9,12,32,26.3,45.1,42.4l19.4-14.8C685.7,327.2,668.6,311.2,649.5,297.7z"/>
<path class="st1" d="M672.7,633.2c12-16.1,21.8-33.7,29.1-52.5l-21.5-7.7c-6.4,16.4-15,31.9-25.5,46L672.7,633.2z"/>
<path class="st2" d="M690.6,449.6l-21.6,7.2c6,20.7,8,42.4,6,63.8c-1.1,11.9-3.4,23.7-6.9,35.2l21.5,7.6c4.1-13.2,6.9-26.9,8.2-40.7 C700.1,498.1,697.6,473.3,690.6,449.6z"/>
<path class="st2" d="M475.2,698l2.1-22.7c-13.3-2-26.4-5.5-38.9-10.5l-9.4,20.7C443.8,691.5,459.3,695.7,475.2,698z"/>
<path class="st2" d="M631.8,456.2l20.4-6.9c-4.9-12.9-11.4-25.2-19.4-36.6l-17.1,13C622.3,435.2,627.7,445.4,631.8,456.2z"/>
<path class="st2" d="M508.4,345.7h-11.2l1.5,21.4c11.5-0.3,22.9,0.7,34.2,3.2l5.5-20.7c-6.8-1.5-13.6-2.6-20.5-3.2 C514.8,346.1,511.6,345.9,508.4,345.7z"/>
<path class="st2" d="M335.5,403.8l20,11.1c7.5-12.4,16.5-23.7,26.9-33.8L367,364.1C354.8,375.9,344.2,389.2,335.5,403.8z"/>
<path class="st2" d="M553.8,339.5c13.8,4.2,27.1,10.2,39.4,17.7l12.7-19c-14.4-8.9-30-15.8-46.2-20.7L553.8,339.5z"/>
<path class="st2" d="M635.9,394.5l18.1-13.8c-10.7-13.2-23.2-24.9-36.9-34.8l-12.7,19C616.2,373.4,626.7,383.3,635.9,394.5z"/>
<path class="st2" d="M611.5,584.6l16.8,13.4c8.2-11.2,14.9-23.3,20.1-36.2l-20.2-7.2C623.8,565.2,618.2,575.3,611.5,584.6z"/>
<path class="st2" d="M389.9,635.1l-15.6,16.6c12.8,11.2,26.9,20.7,42.2,28.2l9.4-20.7C412.9,652.8,400.8,644.6,389.9,635.1z"/>
<path class="st2" d="M369.2,520.2c-1-9.7-1.1-19.5-0.2-29.2c0.2-1.7,0.4-3.5,0.6-5.1l-21.1-3.8c-0.3,2.3-0.6,4.6-0.8,6.9 c-1.1,11.5-0.9,23,0.3,34.5L369.2,520.2z"/>
<path class="st2" d="M333.6,538l-22.6,3.5c3.2,16.7,8.6,33,16,48.3l20.2-10.7C340.9,566,336.4,552.2,333.6,538z"/>
<path class="st2" d="M601.7,646.3l12.3,19.2c14-9.6,26.7-21,37.7-33.8l-17.9-14.2C624.4,628.4,613.6,638.1,601.7,646.3z"/>
<path class="st2" d="M348.8,426.9l-19.9-11c-7.8,15.1-13.5,31.2-17,47.8l22.5,4C337.4,453.5,342.2,439.8,348.8,426.9z"/>
<path class="st2" d="M540.6,636.9l5,20.7c13.3-3.8,26.1-9.2,38.1-16.2l-11.6-18.1C562.2,629,551.6,633.6,540.6,636.9z"/>
<path class="st2" d="M384,573.5l-19,9.9c6.9,12,15.4,23,25.1,32.9l14.8-15.7C396.9,592.4,389.9,583.3,384,573.5z"/>
<path class="st2" d="M496.7,677.1c-1.9,0-3.8-0.2-5.7-0.4l-2.1,22.7c17.9,1.3,35.9,0.1,53.4-3.5l-5.3-22.2 C523.8,676.5,510.2,677.6,496.7,677.1z"/>
<path class="st2" d="M377.3,354.9l15.3,16.9c11.1-9.3,23.3-17.1,36.4-23.3l-9-21C404.6,334.7,390.3,343.9,377.3,354.9z"/>
<path class="st2" d="M432.7,322.1l9,21c13.5-5.2,27.6-8.7,42-10.3L482,310C465.1,311.9,448.5,315.9,432.7,322.1z"/>
<path class="st1" d="M490.3,757.5c21.5,0.7,43-1.1,64.2-5.2l-5-23.3c-18.3,3.8-37,5.3-55.8,4.6c-3,0-5.2-0.4-8.2-0.6l-2.1,24.4 c2.3,0.1,4.6,0.1,6.9,0L490.3,757.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400px" height="400px" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Exodus_logo</title>
<defs>
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
<stop stop-color="#00BFFF" offset="0%"></stop>
<stop stop-color="#6619FF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Exodus_logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Exodus-logo">
<rect id="Rectangle" fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" id="01-Exodus-wallet" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="400px" height="400px" viewBox="0 0 400 400" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="exodus">
<rect id="Rectangle" fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" id="Fill-1" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 916 B

View File

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360">
<rect style="fill: white" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)" id="fdfc8ede-1ea7-4fcd-b8cd-38f5fb196623" >
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-115 -15 879 679" style="background-color: rgb(27,20,100)">
<defs>
<style>.cls-1{fill:url(#linear-gradient);}</style>
<linearGradient id="linear-gradient" x1="81.36" y1="311.35" x2="541.35" y2="311.35" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="blue"/>
<stop offset="1" stop-color="#f0f"/>
</linearGradient>
</defs>
<path class="cls-1" d="M326.4,572.09C201.2,572.09,141,503,112.48,445,84.26,387.47,81.89,330.44,81.69,322.31c-4.85-77,41-231.78,249.58-271.2a28.05,28.05,0,0,1,10.41,55.13c-213.12,40.28-204.44,206-204,213,0,.53.06,1.06.07,1.6C137.9,328.74,142.85,516,326.4,516,394.74,516,443,486.6,470,428.63c24.48-52.74,19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46,0-63.47,9.88-86.21,29.37A91.5,91.5,0,0,0,248,322.3c-1.41,25.4,7.14,49.36,24.07,67.49C287.27,406,305,413.9,326.4,413.9c27.46,0,45.52-9,53.66-26.81,8.38-18.3,3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78,4.61-8.48,10.92a28,28,0,0,1-51.58-22c14.28-33.44,37.94-42,50.76-44.2,24.78-4.18,52.17,7.3,73.34,30.65s25.51,68.55,10.15,103.22C421.54,432,394.52,470,326.4,470c-36.72,0-69.67-14.49-95.29-41.92C203.64,398.68,189.77,360,192,319.19a149.1,149.1,0,0,1,51.31-104.6c33.19-28.45,74.48-42.87,122.71-42.87,55.12,0,101.85,23.25,135.12,67.23,45.36,60,52.9,141.71,19.66,213.3C495.45,506.92,441.12,572.09,326.4,572.09Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 216 216" style="enable-background:new 0 0 216 216;" xml:space="preserve">
<style type="text/css">
.st0{fill:#002248;}
.st1{opacity:0.5;fill:#FFFFFF;}
.st2{fill:#FFFFFF;}
.st3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="st0" width="216" height="216"/>
<g>
<g>
<path class="st1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2
c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7
l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="st2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4
c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1
c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="st3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7
c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7
c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -21,6 +21,18 @@ do for url in / \
'/api/v1/mining/pools/2y' \ '/api/v1/mining/pools/2y' \
'/api/v1/mining/pools/3y' \ '/api/v1/mining/pools/3y' \
'/api/v1/mining/pools/all' \ '/api/v1/mining/pools/all' \
'/api/v1/mining/hashrate/3m' \
'/api/v1/mining/hashrate/6m' \
'/api/v1/mining/hashrate/1y' \
'/api/v1/mining/hashrate/2y' \
'/api/v1/mining/hashrate/3y' \
'/api/v1/mining/hashrate/all' \
'/api/v1/mining/hashrate/pools/3m' \
'/api/v1/mining/hashrate/pools/6m' \
'/api/v1/mining/hashrate/pools/1y' \
'/api/v1/mining/hashrate/pools/2y' \
'/api/v1/mining/hashrate/pools/3y' \
'/api/v1/mining/hashrate/pools/all' \
do do
curl -s "https://${hostname}${url}" >/dev/null curl -s "https://${hostname}${url}" >/dev/null