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,
"USE_SECOND_NODE_FOR_MINFEE": false,
"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_BACKEND: ""

View File

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

View File

@ -6,7 +6,7 @@ import logger from '../logger';
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
class DatabaseMigration {
private static currentVersion = 6;
private static currentVersion = 8;
private queryTimeout = 120000;
private statisticsAddedIndexed = false;
@ -15,13 +15,13 @@ class DatabaseMigration {
* Entry point
*/
public async $initializeOrMigrateDatabase(): Promise<void> {
logger.info('MIGRATIONS: Running migrations');
logger.debug('MIGRATIONS: Running migrations');
await this.$printDatabaseVersion();
// First of all, if the `state` database does not exist, create it so we can track migration version
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 {
await this.$createMigrationStateTable();
} catch (e) {
@ -29,7 +29,7 @@ class DatabaseMigration {
await sleep(10000);
process.exit(-1);
}
logger.info('MIGRATIONS: `state` table initialized.');
logger.debug('MIGRATIONS: `state` table initialized.');
}
let databaseSchemaVersion = 0;
@ -41,10 +41,10 @@ class DatabaseMigration {
process.exit(-1);
}
logger.info('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
logger.info('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
logger.info('MIGRATIONS: Nothing to do.');
logger.debug('MIGRATIONS: Nothing to do.');
return;
}
@ -58,10 +58,10 @@ class DatabaseMigration {
}
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
logger.info('MIGRATIONS: Upgrading datababse schema');
logger.notice('MIGRATIONS: Upgrading datababse schema');
try {
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) {
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'));
}
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, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 6 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
// Cleanup original blocks fields type
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
@ -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 `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();
} catch (e) {
connection.release();
@ -143,10 +160,10 @@ class DatabaseMigration {
WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
const [rows] = await this.$executeQuery(connection, query, true);
if (rows[0].hasIndex === 0) {
logger.info('MIGRATIONS: `statistics.added` is not indexed');
logger.debug('MIGRATIONS: `statistics.added` is not indexed');
this.statisticsAddedIndexed = false;
} 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;
}
} catch (e) {
@ -164,7 +181,7 @@ class DatabaseMigration {
*/
private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> {
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 });
}
@ -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;
}
@ -272,9 +293,9 @@ class DatabaseMigration {
const connection = await DB.pool.getConnection();
try {
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) {
logger.info(`MIGRATIONS: Could not fetch database engine version. ` + e);
logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
}
connection.release();
}
@ -398,6 +419,40 @@ class DatabaseMigration {
FOREIGN KEY (pool_id) REFERENCES pools (id)
) 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();

View File

@ -1,16 +1,21 @@
import { PoolInfo, PoolStats } from '../mempool.interfaces';
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import blocks from './blocks';
class Mining {
hashrateIndexingStarted = false;
constructor() {
}
/**
* 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 poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
@ -26,8 +31,8 @@ class Mining {
link: poolInfo.link,
blockCount: poolInfo.blockCount,
rank: rank++,
emptyBlocks: 0,
}
emptyBlocks: 0
};
for (let i = 0; i < emptyBlocks.length; ++i) {
if (emptyBlocks[i].poolId === poolInfo.poolId) {
poolStat.emptyBlocks++;
@ -37,15 +42,13 @@ class Mining {
});
poolsStatistics['pools'] = poolsStats;
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
const blockCount: number = await BlocksRepository.$blockCount(null, interval);
poolsStatistics['blockCount'] = blockCount;
const blockHeightTip = await bitcoinClient.getBlockCount();
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip);
poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
return poolsStatistics;
@ -74,13 +77,137 @@ class Mining {
* Return the historical difficulty adjustments and oldest indexed block timestamp
*/
public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval);
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
return {
adjustments: difficultyAdjustments,
oldestIndexedBlockTimestamp: oldestBlock.getTime(),
return await BlocksRepository.$getBlocksDifficulty(interval);
}
/**
* Return the historical hashrates and oldest indexed block timestamp
*/
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 icons from './api/liquid/icons';
import { Common } from './api/common';
import mining from './api/mining';
import HashratesRepository from './repositories/HashratesRepository';
class Server {
private wss: WebSocket.Server | undefined;
@ -88,6 +90,13 @@ class Server {
if (config.DATABASE.ENABLED) {
await checkDbConnection();
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 poolsParser.migratePoolsJson();
} catch (e) {
@ -138,7 +147,7 @@ class Server {
}
await blocks.$updateBlocks();
await memPool.$updateMempool();
blocks.$generateBlockDatabase();
this.$runIndexingWhenReady();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
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() {
if (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/:interval', routes.$getPool)
.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) {

View File

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

View File

@ -149,16 +149,49 @@ class BlocksRepository {
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
*/
public async $oldestBlockTimestamp(): Promise<number> {
const query = `SELECT blockTimestamp
const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
FROM blocks
ORDER BY height
LIMIT 1;`;
// logger.debug(query);
const connection = await DB.pool.getConnection();
const [rows]: any[] = await connection.query(query);
@ -232,21 +265,54 @@ class BlocksRepository {
const connection = await DB.pool.getConnection();
let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height
FROM blocks`;
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
// Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY difficulty
ORDER BY blockTimestamp DESC`;
query += `
ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
const [rows]: any[] = await connection.query(query);
connection.release();
for (let row of rows) {
delete row['rn'];
}
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();

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;
}
/**
* 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
*/

View File

@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser';
import icons from './api/liquid/icons';
import miningStats from './api/mining';
import axios from 'axios';
import PoolsRepository from './repositories/PoolsRepository';
import mining from './api/mining';
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) {
try {
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_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_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_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('.footer').should('be.visible');
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) => {
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: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"
},
"dependencies": {
"@angular-devkit/build-angular": "^13.1.2",
"@angular/animations": "~13.1.1",
"@angular/cli": "~13.0.4",
"@angular/common": "~13.1.1",
"@angular/compiler": "~13.1.1",
"@angular/core": "~13.1.1",
"@angular/forms": "~13.1.1",
"@angular/localize": "^13.1.1",
"@angular/platform-browser": "~13.1.1",
"@angular/platform-browser-dynamic": "~13.1.1",
"@angular/platform-server": "~13.1.1",
"@angular/router": "~13.1.1",
"@fortawesome/angular-fontawesome": "^0.8.2",
"@fortawesome/fontawesome-common-types": "^0.2.35",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@angular-devkit/build-angular": "^13.2.4",
"@angular/animations": "~13.2.3",
"@angular/cli": "~13.2.4",
"@angular/common": "~13.2.3",
"@angular/compiler": "~13.2.3",
"@angular/core": "~13.2.3",
"@angular/forms": "~13.2.3",
"@angular/localize": "^13.2.3",
"@angular/platform-browser": "~13.2.3",
"@angular/platform-browser-dynamic": "~13.2.3",
"@angular/platform-server": "~13.2.3",
"@angular/router": "~13.2.3",
"@fortawesome/angular-fontawesome": "0.10.1",
"@fortawesome/fontawesome-common-types": "0.3.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-solid-svg-icons": "6.0.0",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "2.3.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",
"bootstrap": "4.5.0",
"browserify": "^17.0.0",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"echarts": "^5.1.2",
"echarts": "5.3.0",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "^7.0.1",
"ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0",
"rxjs": "^6.6.7",
@ -101,9 +100,9 @@
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular/compiler-cli": "~13.1.1",
"@angular/language-service": "~13.1.1",
"@nguniversal/builders": "^11.2.1",
"@angular/compiler-cli": "~13.2.3",
"@angular/language-service": "~13.2.3",
"@nguniversal/builders": "~13.0.2",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
@ -123,7 +122,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.3.1",
"cypress": "^9.5.0",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",

View File

@ -42,7 +42,28 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
pathRewrite: {
"^/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,
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 { PoolComponent } from './components/pool/pool.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 = [
{
@ -70,17 +72,32 @@ let routes: Routes = [
component: LatestBlocksComponent,
},
{
path: 'mining/difficulty',
component: DifficultyChartComponent,
path: 'mining',
component: MiningStartComponent,
children: [
{
path: 'hashrate',
component: HashrateChartComponent,
},
{
path: 'mining/pools',
path: 'hashrate/pools',
component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'mining/pool/:poolId',
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
},
{
path: 'graphs',
component: StatisticsComponent,
@ -170,17 +187,32 @@ let routes: Routes = [
component: LatestBlocksComponent,
},
{
path: 'mining/difficulty',
component: DifficultyChartComponent,
path: 'mining',
component: MiningStartComponent,
children: [
{
path: 'hashrate',
component: HashrateChartComponent,
},
{
path: 'mining/pools',
path: 'hashrate/pools',
component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'mining/pool/:poolId',
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
},
{
path: 'graphs',
component: StatisticsComponent,
@ -264,17 +296,32 @@ let routes: Routes = [
component: LatestBlocksComponent,
},
{
path: 'mining/difficulty',
component: DifficultyChartComponent,
path: 'mining',
component: MiningStartComponent,
children: [
{
path: 'hashrate',
component: HashrateChartComponent,
},
{
path: 'mining/pools',
path: 'hashrate/pools',
component: HashrateChartPoolsComponent,
},
{
path: 'pools',
component: PoolRankingComponent,
},
{
path: 'mining/pool/:poolId',
path: 'pool',
children: [
{
path: ':poolId',
component: PoolComponent,
},
]
},
]
},
{
path: 'graphs',
component: StatisticsComponent,

View File

@ -71,7 +71,23 @@ export const chartColors = [
"#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];
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 { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.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 { 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({
declarations: [
@ -120,8 +124,12 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul
AssetsNavComponent,
AssetsFeaturedComponent,
AssetGroupComponent,
AssetCirculationComponent,
MiningDashboardComponent,
DifficultyChartComponent,
HashrateChartComponent,
HashrateChartPoolsComponent,
MiningStartComponent,
AmountShortenerPipe,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),

View File

@ -69,7 +69,7 @@ export function calcSegwitFeeGains(tx: Transaction) {
export function moveDec(num: number, n: number) {
let frac, int, neg, ref;
if (n === 0) {
return num;
return num.toString();
}
ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
int || (int = '0');
@ -130,3 +130,32 @@ export const formatNumber = (s, precision = null) => {
// Utilities for segwitFeeGains
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;
// 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 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>
<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>
<ng-template #marginBox>
<div class="no-about-margin"></div>
</ng-template>
<div class="social-icons">
<a target="_blank" href="https://github.com/mempool/mempool">
@ -31,31 +28,128 @@
</a>
</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">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper">
<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>
</a>
<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>
</a>
<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>
</a>
<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>
</a>
<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>
</a>
<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>
</a>
</div>
@ -73,9 +167,6 @@
</ng-template>
</ng-container>
</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 class="community-integrations-sponsor">
@ -106,6 +197,10 @@
<img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span>
</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">
<img class="image" src="/resources/profile/electrum.jpg" />
<span>Electrum</span>
@ -142,10 +237,6 @@
<img class="image" src="/resources/profile/marina.svg" />
<span>Marina</span>
</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>

View File

@ -115,15 +115,15 @@
display: inline-block;
&:hover {
text-decoration: none;
img {
img, svg {
transform: scale(1.1);
}
}
img, span{
img, svg, span {
display: block;
transition: 150ms all;
}
img {
img, svg {
margin: 40px 29px 10px;
}
}

View File

@ -46,53 +46,68 @@ export class AddressLabelsComponent implements OnInit {
return;
}
[
// {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'},
{regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'}
].forEach((item) => {
if (item.regexp.test(this.vin.inner_witnessscript_asm)) {
this.lightning = item.label;
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
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)) {
if (this.vin.witness[this.vin.witness.length - 2] == '01') {
this.lightning = 'Revoked Force Close';
} else {
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) {
return;
}
if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
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.multisig = false;
}
}
this.detectMultisig(this.vin.inner_witnessscript_asm);
}
if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
this.multisig = true;
this.multisigM = matches[0];
this.multisigN = matches[1];
this.detectMultisig(this.vin.inner_redeemscript_asm);
}
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 (!opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0]);
this.multisig = true;
this.multisigM = m;
this.multisigN = n;
}
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">
{{ 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 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">
<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>
@ -21,7 +24,7 @@
</div>
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></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]">
{{ block.extras.pool.name}}</a>
</div>

View File

@ -130,3 +130,14 @@
top: 15px;
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 { StateService } from 'src/app/services/state.service';
import { Router } from '@angular/router';
import { specialBlocks } from 'src/app/app.constants';
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { Location } from '@angular/common';
import { config } from 'process';
@Component({
selector: 'app-blockchain-blocks',
@ -12,7 +13,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
@Input() showMiningInfo: boolean = false;
specialBlocks = specialBlocks;
network = '';
blocks: BlockExtended[] = [];
@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
arrowLeftPx = 30;
blocksFilled = false;
transition = '1s';
showMiningInfo = false;
gradientColors = {
'': ['#9339f4', '#105fb0'],
@ -44,11 +45,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
constructor(
public stateService: StateService,
private router: Router,
private cd: ChangeDetectorRef,
) { }
private location: Location,
) {
}
enabledMiningInfoIfNeeded(url) {
this.showMiningInfo = url === '/mining';
this.cd.markForCheck(); // Need to update the view asap
}
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') {
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 }}">
<span>
<app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks [showMiningInfo]="showMiningInfo"></app-blockchain-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
<div id="divider"></div>
</span>
</div>

View File

@ -60,13 +60,3 @@
left: -150px;
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,
})
export class BlockchainComponent implements OnInit {
showMiningInfo: boolean = false;
network: string;
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="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
<div class="col d-none d-sm-block">
<span class="txPerSecond" i18n="footer.tx-vbytes-per-second">Tx vBytes per second:</span>
<span *ngIf="mempoolInfoData.vBytesPerSecond === 0; else inSync">
&nbsp;<span class="badge badge-pill badge-warning" i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</span>
<span class="txPerSecond" i18n="dashboard.incoming-transactions">Incoming transactions</span>&nbsp;
<ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData" [ngIfElse]="loadingTransactions">
<span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
&nbsp;<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
</span>
<ng-template #inSync>
<div class="progress sub-text">
<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 class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}">&nbsp;</div>
<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 class="col">
<span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span>
@ -25,3 +28,7 @@
</div>
</div>
</footer>
<ng-template #loadingTransactions>
<div class="skeleton-loader skeleton-loader-transactions"></div>
</ng-template>

View File

@ -13,7 +13,7 @@ interface MempoolInfoData {
memPoolInfo: MempoolInfo;
vBytesPerSecond: number;
progressWidth: string;
progressClass: string;
progressColor: string;
}
@Component({
@ -26,12 +26,20 @@ export class FooterComponent implements OnInit {
mempoolBlocksData$: Observable<MempoolBlocksData>;
mempoolInfoData$: Observable<MempoolInfoData>;
vBytesPerSecondLimit = 1667;
isLoadingWebSocket$: Observable<boolean>;
mempoolLoadingStatus$: Observable<number>;
constructor(
private stateService: StateService,
) { }
ngOnInit() {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$,
this.stateService.vbytesPerSecond$
@ -40,18 +48,37 @@ export class FooterComponent implements OnInit {
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressClass = 'bg-danger';
if (percent <= 75) {
progressClass = 'bg-success';
} else if (percent <= 99) {
progressClass = 'bg-warning';
let progressColor = '#7CB342';
if (vbytesPerSecond > 1667) {
progressColor = '#FDD835';
}
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 + '%',
progressClass: progressClass,
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
);

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',
smooth: false,
showSymbol: false,
symbol: 'none',
lineStyle: {
width: 3,
},

View File

@ -31,7 +31,7 @@
<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>
</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>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">

View File

@ -12,7 +12,10 @@
<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>
</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">
<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>

View File

@ -7,6 +7,7 @@ import { take, map, switchMap } from 'rxjs/operators';
import { feeLevels, mempoolFeeColors } 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 { Location } from '@angular/common';
@Component({
selector: 'app-mempool-blocks',
@ -32,6 +33,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
networkSubscription: Subscription;
network = '';
now = new Date().getTime();
showMiningInfo = false;
blockWidth = 125;
blockPadding = 30;
@ -54,9 +56,20 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
public stateService: StateService,
private cd: ChangeDetectorRef,
private relativeUrlPipe: RelativeUrlPipe,
private location: Location
) { }
enabledMiningInfoIfNeeded(url) {
this.showMiningInfo = url === '/mining';
this.cd.markForCheck(); // Need to update the view asap
}
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') {
this.feeRounding = '1.0-1';
}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
@Component({
selector: 'app-mining-dashboard',
@ -8,7 +9,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
})
export class MiningDashboardComponent implements OnInit {
constructor() { }
constructor(private seoService: SeoService) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
}
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="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div [class]="widget ? 'chart-widget' : 'chart'"
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>
<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">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">

View File

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

View File

@ -172,9 +172,12 @@
</td>
<td class="text-right nowrap amount">
<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>
</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 #defaultOutput>
<app-amount [satoshis]="vout.value"></app-amount>
@ -254,7 +257,7 @@
&nbsp;
</span>
<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>
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
</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) {
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
}

View File

@ -16,20 +16,21 @@
<div class="col">
<div class="card">
<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 class="col">
<div class="card">
<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>
</ng-template>
<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="col card-wrapper">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body">
@ -37,16 +38,18 @@
</div>
</div>
</div>
<div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<div class="col">
<app-difficulty></app-difficulty>
</div>
</ng-container>
<div class="col">
<div class="card graph-card">
<div class="card-body pl-0">
<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>
</div>
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
<div class="mempool-graph">
<app-mempool-graph
@ -58,16 +61,34 @@
></app-mempool-graph>
</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 class="col">
<div class="card graph-card">
<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>
<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>
<ng-template #mempoolGraph>
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
@ -158,6 +179,27 @@
</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>
<div class="skeleton-loader skeleton-loader-transactions"></div>
</ng-template>

View File

@ -283,3 +283,25 @@
margin-right: -2px;
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 { 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 { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
@ -34,6 +34,7 @@ interface MempoolStatsData {
})
export class DashboardComponent implements OnInit {
collapseLevel: string;
featuredAssets$: Observable<any>;
network$: Observable<string>;
mempoolBlocksData$: Observable<MempoolBlocksData>;
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$
.pipe(
tap(([block]) => {

View File

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

View File

@ -156,4 +156,18 @@ export class ApiService {
(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 = (
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp / 1000)
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp)
) / 3600 / 24;
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/3y' \
'/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
curl -s "https://${hostname}${url}" >/dev/null