Merge branch 'master' into fee-visibility
@ -91,11 +91,11 @@ JSON:
|
|||||||
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": false,
|
"USE_SECOND_NODE_FOR_MINFEE": false,
|
||||||
"EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"],
|
"EXTERNAL_ASSETS": ["https://mempool.space/resources/pools.json"],
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "debug"
|
"STDOUT_LOG_MIN_PRIORITY": "info"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
docker-compose overrides::
|
docker-compose overrides:
|
||||||
```
|
```
|
||||||
MEMPOOL_NETWORK: ""
|
MEMPOOL_NETWORK: ""
|
||||||
MEMPOOL_BACKEND: ""
|
MEMPOOL_BACKEND: ""
|
||||||
|
@ -20,6 +20,7 @@ class Blocks {
|
|||||||
private previousDifficultyRetarget = 0;
|
private previousDifficultyRetarget = 0;
|
||||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
private blockIndexingStarted = false;
|
private blockIndexingStarted = false;
|
||||||
|
public blockIndexingCompleted = false;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -115,6 +116,9 @@ class Blocks {
|
|||||||
Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
Common.median(transactionsTmp.map((tx) => tx.effectiveFeePerVsize)) : 0;
|
||||||
blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
|
blockExtended.extras.feeRange = transactionsTmp.length > 0 ?
|
||||||
Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
|
Common.getFeesInRange(transactionsTmp, 8) : [0, 0];
|
||||||
|
blockExtended.extras.totalFees = transactionsTmp.reduce((acc, tx) => {
|
||||||
|
return acc + tx.fee;
|
||||||
|
}, 0)
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
let pool: PoolTag;
|
let pool: PoolTag;
|
||||||
@ -170,10 +174,7 @@ class Blocks {
|
|||||||
* Index all blocks metadata for the mining dashboard
|
* Index all blocks metadata for the mining dashboard
|
||||||
*/
|
*/
|
||||||
public async $generateBlockDatabase() {
|
public async $generateBlockDatabase() {
|
||||||
if (this.blockIndexingStarted === true ||
|
if (this.blockIndexingStarted) {
|
||||||
!Common.indexingEnabled() ||
|
|
||||||
memPool.hasPriority()
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +244,8 @@ class Blocks {
|
|||||||
logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e);
|
logger.err('An error occured in $generateBlockDatabase(). Skipping block indexing. ' + e);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.blockIndexingCompleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlocks() {
|
public async $updateBlocks() {
|
||||||
|
@ -6,7 +6,7 @@ import logger from '../logger';
|
|||||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 6;
|
private static currentVersion = 8;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
|
|
||||||
@ -15,13 +15,13 @@ class DatabaseMigration {
|
|||||||
* Entry point
|
* Entry point
|
||||||
*/
|
*/
|
||||||
public async $initializeOrMigrateDatabase(): Promise<void> {
|
public async $initializeOrMigrateDatabase(): Promise<void> {
|
||||||
logger.info('MIGRATIONS: Running migrations');
|
logger.debug('MIGRATIONS: Running migrations');
|
||||||
|
|
||||||
await this.$printDatabaseVersion();
|
await this.$printDatabaseVersion();
|
||||||
|
|
||||||
// First of all, if the `state` database does not exist, create it so we can track migration version
|
// First of all, if the `state` database does not exist, create it so we can track migration version
|
||||||
if (!await this.$checkIfTableExists('state')) {
|
if (!await this.$checkIfTableExists('state')) {
|
||||||
logger.info('MIGRATIONS: `state` table does not exist. Creating it.');
|
logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
|
||||||
try {
|
try {
|
||||||
await this.$createMigrationStateTable();
|
await this.$createMigrationStateTable();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -29,7 +29,7 @@ class DatabaseMigration {
|
|||||||
await sleep(10000);
|
await sleep(10000);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
logger.info('MIGRATIONS: `state` table initialized.');
|
logger.debug('MIGRATIONS: `state` table initialized.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let databaseSchemaVersion = 0;
|
let databaseSchemaVersion = 0;
|
||||||
@ -41,10 +41,10 @@ class DatabaseMigration {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
|
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
|
||||||
logger.info('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
|
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
|
||||||
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
|
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
|
||||||
logger.info('MIGRATIONS: Nothing to do.');
|
logger.debug('MIGRATIONS: Nothing to do.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,10 +58,10 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
|
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
|
||||||
logger.info('MIGRATIONS: Upgrading datababse schema');
|
logger.notice('MIGRATIONS: Upgrading datababse schema');
|
||||||
try {
|
try {
|
||||||
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
|
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
|
||||||
logger.info(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
|
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
|
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
|
||||||
}
|
}
|
||||||
@ -92,11 +92,13 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
await this.$executeQuery(connection, this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
|
||||||
}
|
}
|
||||||
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
if (databaseSchemaVersion < 5 && isBitcoin === true) {
|
||||||
|
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
|
||||||
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
||||||
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
if (databaseSchemaVersion < 6 && isBitcoin === true) {
|
||||||
|
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.'`);
|
||||||
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
await this.$executeQuery(connection, 'TRUNCATE blocks;'); // Need to re-index
|
||||||
// Cleanup original blocks fields type
|
// Cleanup original blocks fields type
|
||||||
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
await this.$executeQuery(connection, 'ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
|
||||||
@ -116,6 +118,21 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
|
||||||
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 7 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;');
|
||||||
|
await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 8 && isBitcoin === true) {
|
||||||
|
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.'`);
|
||||||
|
await this.$executeQuery(connection, 'TRUNCATE hashrates;'); // Need to re-index
|
||||||
|
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
|
||||||
|
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
|
||||||
|
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
|
||||||
|
await this.$executeQuery(connection, 'ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
|
||||||
|
}
|
||||||
|
|
||||||
connection.release();
|
connection.release();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
connection.release();
|
connection.release();
|
||||||
@ -143,10 +160,10 @@ class DatabaseMigration {
|
|||||||
WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
|
WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
|
||||||
const [rows] = await this.$executeQuery(connection, query, true);
|
const [rows] = await this.$executeQuery(connection, query, true);
|
||||||
if (rows[0].hasIndex === 0) {
|
if (rows[0].hasIndex === 0) {
|
||||||
logger.info('MIGRATIONS: `statistics.added` is not indexed');
|
logger.debug('MIGRATIONS: `statistics.added` is not indexed');
|
||||||
this.statisticsAddedIndexed = false;
|
this.statisticsAddedIndexed = false;
|
||||||
} else if (rows[0].hasIndex === 1) {
|
} else if (rows[0].hasIndex === 1) {
|
||||||
logger.info('MIGRATIONS: `statistics.added` is already indexed');
|
logger.debug('MIGRATIONS: `statistics.added` is already indexed');
|
||||||
this.statisticsAddedIndexed = true;
|
this.statisticsAddedIndexed = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -164,7 +181,7 @@ class DatabaseMigration {
|
|||||||
*/
|
*/
|
||||||
private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> {
|
private async $executeQuery(connection: PoolConnection, query: string, silent: boolean = false): Promise<any> {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
logger.info('MIGRATIONS: Execute query:\n' + query);
|
logger.debug('MIGRATIONS: Execute query:\n' + query);
|
||||||
}
|
}
|
||||||
return connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
return connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||||
}
|
}
|
||||||
@ -255,6 +272,10 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 7) {
|
||||||
|
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
|
||||||
|
}
|
||||||
|
|
||||||
return queries;
|
return queries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,9 +293,9 @@ class DatabaseMigration {
|
|||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
try {
|
try {
|
||||||
const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true);
|
const [rows] = await this.$executeQuery(connection, 'SELECT VERSION() as version;', true);
|
||||||
logger.info(`MIGRATIONS: Database engine version '${rows[0].version}'`);
|
logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info(`MIGRATIONS: Could not fetch database engine version. ` + e);
|
logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
|
||||||
}
|
}
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
@ -398,6 +419,40 @@ class DatabaseMigration {
|
|||||||
FOREIGN KEY (pool_id) REFERENCES pools (id)
|
FOREIGN KEY (pool_id) REFERENCES pools (id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateDailyStatsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS hashrates (
|
||||||
|
hashrate_timestamp timestamp NOT NULL,
|
||||||
|
avg_hashrate double unsigned DEFAULT '0',
|
||||||
|
pool_id smallint unsigned NULL,
|
||||||
|
PRIMARY KEY (hashrate_timestamp),
|
||||||
|
INDEX (pool_id),
|
||||||
|
FOREIGN KEY (pool_id) REFERENCES pools (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
|
const allowedTables = ['blocks', 'hashrates'];
|
||||||
|
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
try {
|
||||||
|
for (const table of tables) {
|
||||||
|
if (!allowedTables.includes(table)) {
|
||||||
|
logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.$executeQuery(connection, `TRUNCATE ${table}`, true);
|
||||||
|
if (table === 'hashrates') {
|
||||||
|
await this.$executeQuery(connection, 'UPDATE state set number = 0 where name = "last_hashrates_indexing"', true);
|
||||||
|
}
|
||||||
|
logger.notice(`Table ${table} has been truncated`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Unable to erase indexed data`);
|
||||||
|
}
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import { PoolInfo, PoolStats } from '../mempool.interfaces';
|
import { PoolInfo, PoolStats } from '../mempool.interfaces';
|
||||||
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
|
import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository';
|
||||||
import PoolsRepository from '../repositories/PoolsRepository';
|
import PoolsRepository from '../repositories/PoolsRepository';
|
||||||
|
import HashratesRepository from '../repositories/HashratesRepository';
|
||||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||||
|
import logger from '../logger';
|
||||||
|
import blocks from './blocks';
|
||||||
|
|
||||||
class Mining {
|
class Mining {
|
||||||
|
hashrateIndexingStarted = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate high level overview of the pool ranks and general stats
|
* Generate high level overview of the pool ranks and general stats
|
||||||
*/
|
*/
|
||||||
public async $getPoolsStats(interval: string | null) : Promise<object> {
|
public async $getPoolsStats(interval: string | null): Promise<object> {
|
||||||
const poolsStatistics = {};
|
const poolsStatistics = {};
|
||||||
|
|
||||||
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
|
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
|
||||||
@ -26,8 +31,8 @@ class Mining {
|
|||||||
link: poolInfo.link,
|
link: poolInfo.link,
|
||||||
blockCount: poolInfo.blockCount,
|
blockCount: poolInfo.blockCount,
|
||||||
rank: rank++,
|
rank: rank++,
|
||||||
emptyBlocks: 0,
|
emptyBlocks: 0
|
||||||
}
|
};
|
||||||
for (let i = 0; i < emptyBlocks.length; ++i) {
|
for (let i = 0; i < emptyBlocks.length; ++i) {
|
||||||
if (emptyBlocks[i].poolId === poolInfo.poolId) {
|
if (emptyBlocks[i].poolId === poolInfo.poolId) {
|
||||||
poolStat.emptyBlocks++;
|
poolStat.emptyBlocks++;
|
||||||
@ -37,15 +42,13 @@ class Mining {
|
|||||||
});
|
});
|
||||||
|
|
||||||
poolsStatistics['pools'] = poolsStats;
|
poolsStatistics['pools'] = poolsStats;
|
||||||
|
poolsStatistics['oldestIndexedBlockTimestamp'] = await BlocksRepository.$oldestBlockTimestamp();
|
||||||
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
|
|
||||||
poolsStatistics['oldestIndexedBlockTimestamp'] = oldestBlock.getTime();
|
|
||||||
|
|
||||||
const blockCount: number = await BlocksRepository.$blockCount(null, interval);
|
const blockCount: number = await BlocksRepository.$blockCount(null, interval);
|
||||||
poolsStatistics['blockCount'] = blockCount;
|
poolsStatistics['blockCount'] = blockCount;
|
||||||
|
|
||||||
const blockHeightTip = await bitcoinClient.getBlockCount();
|
const blockHeightTip = await bitcoinClient.getBlockCount();
|
||||||
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip);
|
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip);
|
||||||
poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
|
poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate;
|
||||||
|
|
||||||
return poolsStatistics;
|
return poolsStatistics;
|
||||||
@ -74,13 +77,137 @@ class Mining {
|
|||||||
* Return the historical difficulty adjustments and oldest indexed block timestamp
|
* Return the historical difficulty adjustments and oldest indexed block timestamp
|
||||||
*/
|
*/
|
||||||
public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
|
public async $getHistoricalDifficulty(interval: string | null): Promise<object> {
|
||||||
const difficultyAdjustments = await BlocksRepository.$getBlocksDifficulty(interval);
|
return await BlocksRepository.$getBlocksDifficulty(interval);
|
||||||
const oldestBlock = new Date(await BlocksRepository.$oldestBlockTimestamp());
|
}
|
||||||
|
|
||||||
return {
|
/**
|
||||||
adjustments: difficultyAdjustments,
|
* Return the historical hashrates and oldest indexed block timestamp
|
||||||
oldestIndexedBlockTimestamp: oldestBlock.getTime(),
|
*/
|
||||||
|
public async $getNetworkHistoricalHashrates(interval: string | null): Promise<object> {
|
||||||
|
return await HashratesRepository.$getNetworkDailyHashrate(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the historical hashrates and oldest indexed block timestamp for one or all pools
|
||||||
|
*/
|
||||||
|
public async $getPoolsHistoricalHashrates(interval: string | null, poolId: number): Promise<object> {
|
||||||
|
return await HashratesRepository.$getPoolsWeeklyHashrate(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate daily hashrate data
|
||||||
|
*/
|
||||||
|
public async $generateNetworkHashrateHistory(): Promise<void> {
|
||||||
|
// We only run this once a day
|
||||||
|
const latestTimestamp = await HashratesRepository.$getLatestRunTimestamp();
|
||||||
|
const now = new Date().getTime() / 1000;
|
||||||
|
if (now - latestTimestamp < 86400) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hashrateIndexingStarted = true;
|
||||||
|
|
||||||
|
logger.info(`Indexing hashrates`);
|
||||||
|
|
||||||
|
const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
|
||||||
|
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||||
|
let startedAt = new Date().getTime() / 1000;
|
||||||
|
const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
||||||
|
const lastMidnight = new Date();
|
||||||
|
lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0);
|
||||||
|
let toTimestamp = Math.round(lastMidnight.getTime() / 1000);
|
||||||
|
let indexedThisRun = 0;
|
||||||
|
let totalIndexed = 0;
|
||||||
|
|
||||||
|
const hashrates: any[] = [];
|
||||||
|
|
||||||
|
while (toTimestamp > genesisTimestamp) {
|
||||||
|
const fromTimestamp = toTimestamp - 86400;
|
||||||
|
if (indexedTimestamp.includes(fromTimestamp)) {
|
||||||
|
toTimestamp -= 86400;
|
||||||
|
++totalIndexed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||||
|
null, fromTimestamp, toTimestamp);
|
||||||
|
if (blockStats.blockCount === 0) { // We are done indexing, no blocks left
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastBlockHashrate = 0;
|
||||||
|
lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||||
|
blockStats.lastBlockHeight);
|
||||||
|
|
||||||
|
if (totalIndexed % 7 === 0 && !indexedTimestamp.includes(fromTimestamp + 1)) { // Save weekly pools hashrate
|
||||||
|
logger.debug("Indexing weekly hashrates for mining pools");
|
||||||
|
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp - 604800, fromTimestamp);
|
||||||
|
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
|
||||||
|
pools = pools.map((pool: any) => {
|
||||||
|
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
|
||||||
|
pool.share = (pool.blockCount / totalBlocks);
|
||||||
|
return pool;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const pool of pools) {
|
||||||
|
hashrates.push({
|
||||||
|
hashrateTimestamp: fromTimestamp + 1,
|
||||||
|
avgHashrate: pool['hashrate'],
|
||||||
|
poolId: pool.poolId,
|
||||||
|
share: pool['share'],
|
||||||
|
type: 'weekly',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashrates.push({
|
||||||
|
hashrateTimestamp: fromTimestamp,
|
||||||
|
avgHashrate: lastBlockHashrate,
|
||||||
|
poolId: null,
|
||||||
|
share: 1,
|
||||||
|
type: 'daily',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hashrates.length > 10) {
|
||||||
|
await HashratesRepository.$saveHashrates(hashrates);
|
||||||
|
hashrates.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
|
if (elapsedSeconds > 5) {
|
||||||
|
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
|
||||||
|
const formattedDate = new Date(fromTimestamp * 1000).toUTCString();
|
||||||
|
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
|
||||||
|
logger.debug(`Getting hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ~${daysLeft} days left to index`);
|
||||||
|
startedAt = new Date().getTime() / 1000;
|
||||||
|
indexedThisRun = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
toTimestamp -= 86400;
|
||||||
|
++indexedThisRun;
|
||||||
|
++totalIndexed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add genesis block manually
|
||||||
|
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
|
||||||
|
hashrates.push({
|
||||||
|
hashrateTimestamp: genesisTimestamp,
|
||||||
|
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
|
||||||
|
poolId: null,
|
||||||
|
type: 'daily',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashrates.length > 0) {
|
||||||
|
await HashratesRepository.$saveHashrates(hashrates);
|
||||||
|
}
|
||||||
|
await HashratesRepository.$setLatestRunTimestamp();
|
||||||
|
this.hashrateIndexingStarted = false;
|
||||||
|
|
||||||
|
logger.info(`Hashrates indexing completed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ import poolsParser from './api/pools-parser';
|
|||||||
import syncAssets from './sync-assets';
|
import syncAssets from './sync-assets';
|
||||||
import icons from './api/liquid/icons';
|
import icons from './api/liquid/icons';
|
||||||
import { Common } from './api/common';
|
import { Common } from './api/common';
|
||||||
|
import mining from './api/mining';
|
||||||
|
import HashratesRepository from './repositories/HashratesRepository';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -88,6 +90,13 @@ class Server {
|
|||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
await checkDbConnection();
|
await checkDbConnection();
|
||||||
try {
|
try {
|
||||||
|
if (process.env.npm_config_reindex != undefined) { // Re-index requests
|
||||||
|
const tables = process.env.npm_config_reindex.split(',');
|
||||||
|
logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds from now (using '--reindex') ...`);
|
||||||
|
await Common.sleep(5000);
|
||||||
|
await databaseMigration.$truncateIndexedData(tables);
|
||||||
|
}
|
||||||
|
await this.$resetHashratesIndexingState();
|
||||||
await databaseMigration.$initializeOrMigrateDatabase();
|
await databaseMigration.$initializeOrMigrateDatabase();
|
||||||
await poolsParser.migratePoolsJson();
|
await poolsParser.migratePoolsJson();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -138,7 +147,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
await blocks.$updateBlocks();
|
await blocks.$updateBlocks();
|
||||||
await memPool.$updateMempool();
|
await memPool.$updateMempool();
|
||||||
blocks.$generateBlockDatabase();
|
this.$runIndexingWhenReady();
|
||||||
|
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||||
this.currentBackendRetryInterval = 5;
|
this.currentBackendRetryInterval = 5;
|
||||||
@ -157,6 +166,23 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $resetHashratesIndexingState() {
|
||||||
|
return await HashratesRepository.$setLatestRunTimestamp(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $runIndexingWhenReady() {
|
||||||
|
if (!Common.indexingEnabled() || mempool.hasPriority()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await blocks.$generateBlockDatabase();
|
||||||
|
await mining.$generateNetworkHashrateHistory();
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to run indexing right now, trying again later. ` + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setUpWebsocketHandling() {
|
setUpWebsocketHandling() {
|
||||||
if (this.wss) {
|
if (this.wss) {
|
||||||
websocketHandler.setWebsocketServer(this.wss);
|
websocketHandler.setWebsocketServer(this.wss);
|
||||||
@ -276,7 +302,12 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId', routes.$getPool)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:poolId/:interval', routes.$getPool)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty', routes.$getHistoricalDifficulty)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty);
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty/:interval', routes.$getHistoricalDifficulty)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
|
@ -78,6 +78,7 @@ export interface TransactionStripped {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockExtension {
|
export interface BlockExtension {
|
||||||
|
totalFees?: number;
|
||||||
medianFee?: number;
|
medianFee?: number;
|
||||||
feeRange?: number[];
|
feeRange?: number[];
|
||||||
reward?: number;
|
reward?: number;
|
||||||
|
@ -149,16 +149,49 @@ class BlocksRepository {
|
|||||||
return <number>rows[0].blockCount;
|
return <number>rows[0].blockCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get blocks count between two dates
|
||||||
|
* @param poolId
|
||||||
|
* @param from - The oldest timestamp
|
||||||
|
* @param to - The newest timestamp
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise<number> {
|
||||||
|
const params: any[] = [];
|
||||||
|
let query = `SELECT
|
||||||
|
count(height) as blockCount,
|
||||||
|
max(height) as lastBlockHeight
|
||||||
|
FROM blocks`;
|
||||||
|
|
||||||
|
if (poolId) {
|
||||||
|
query += ` WHERE pool_id = ?`;
|
||||||
|
params.push(poolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poolId) {
|
||||||
|
query += ` AND`;
|
||||||
|
} else {
|
||||||
|
query += ` WHERE`;
|
||||||
|
}
|
||||||
|
query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`;
|
||||||
|
|
||||||
|
// logger.debug(query);
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const [rows] = await connection.query(query, params);
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
return <number>rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the oldest indexed block
|
* Get the oldest indexed block
|
||||||
*/
|
*/
|
||||||
public async $oldestBlockTimestamp(): Promise<number> {
|
public async $oldestBlockTimestamp(): Promise<number> {
|
||||||
const query = `SELECT blockTimestamp
|
const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
|
||||||
FROM blocks
|
FROM blocks
|
||||||
ORDER BY height
|
ORDER BY height
|
||||||
LIMIT 1;`;
|
LIMIT 1;`;
|
||||||
|
|
||||||
|
|
||||||
// logger.debug(query);
|
// logger.debug(query);
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
@ -232,21 +265,54 @@ class BlocksRepository {
|
|||||||
|
|
||||||
const connection = await DB.pool.getConnection();
|
const connection = await DB.pool.getConnection();
|
||||||
|
|
||||||
let query = `SELECT MIN(UNIX_TIMESTAMP(blockTimestamp)) as timestamp, difficulty, height
|
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
|
||||||
FROM blocks`;
|
// Basically, using temporary user defined fields, we are able to extract all
|
||||||
|
// difficulty adjustments from the blocks tables.
|
||||||
|
// This allow use to avoid indexing it in another table.
|
||||||
|
let query = `
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
|
||||||
|
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
|
||||||
|
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
|
||||||
|
) AS rn
|
||||||
|
FROM blocks YT
|
||||||
|
CROSS JOIN
|
||||||
|
(
|
||||||
|
SELECT @prevStatus := -1, @rn := 1
|
||||||
|
) AS var
|
||||||
|
`;
|
||||||
|
|
||||||
if (interval) {
|
if (interval) {
|
||||||
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ` GROUP BY difficulty
|
query += `
|
||||||
ORDER BY blockTimestamp DESC`;
|
ORDER BY YT.height
|
||||||
|
) AS t
|
||||||
|
WHERE t.rn = 1
|
||||||
|
ORDER BY t.height
|
||||||
|
`;
|
||||||
|
|
||||||
const [rows]: any[] = await connection.query(query);
|
const [rows]: any[] = await connection.query(query);
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
||||||
|
for (let row of rows) {
|
||||||
|
delete row['rn'];
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getOldestIndexedBlockHeight(): Promise<number> {
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const [rows]: any[] = await connection.query(`SELECT MIN(height) as minHeight FROM blocks`);
|
||||||
|
connection.release();
|
||||||
|
return rows[0].minHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksRepository();
|
export default new BlocksRepository();
|
||||||
|
102
backend/src/repositories/HashratesRepository.ts
Normal 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();
|
@ -49,6 +49,22 @@ class PoolsRepository {
|
|||||||
return <PoolInfo[]>rows;
|
return <PoolInfo[]>rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic pool info and block count between two timestamp
|
||||||
|
*/
|
||||||
|
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
|
||||||
|
let query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
|
||||||
|
FROM pools
|
||||||
|
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
|
||||||
|
GROUP BY pools.id`;
|
||||||
|
|
||||||
|
const connection = await DB.pool.getConnection();
|
||||||
|
const [rows] = await connection.query(query, [from, to]);
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
return <PoolInfo[]>rows;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mining pool statistics for one pool
|
* Get mining pool statistics for one pool
|
||||||
*/
|
*/
|
||||||
|
@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser';
|
|||||||
import icons from './api/liquid/icons';
|
import icons from './api/liquid/icons';
|
||||||
import miningStats from './api/mining';
|
import miningStats from './api/mining';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import PoolsRepository from './repositories/PoolsRepository';
|
|
||||||
import mining from './api/mining';
|
import mining from './api/mining';
|
||||||
import BlocksRepository from './repositories/BlocksRepository';
|
import BlocksRepository from './repositories/BlocksRepository';
|
||||||
|
|
||||||
@ -587,6 +586,40 @@ class Routes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const hashrates = await mining.$getPoolsHistoricalHashrates(req.params.interval ?? null, parseInt(req.params.poolId, 10));
|
||||||
|
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
|
res.json({
|
||||||
|
oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
|
||||||
|
hashrates: hashrates,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getHistoricalHashrate(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const hashrates = await mining.$getNetworkHistoricalHashrates(req.params.interval ?? null);
|
||||||
|
const difficulty = await mining.$getHistoricalDifficulty(req.params.interval ?? null);
|
||||||
|
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
|
res.json({
|
||||||
|
oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
|
||||||
|
hashrates: hashrates,
|
||||||
|
difficulty: difficulty,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getBlock(req: Request, res: Response) {
|
public async getBlock(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const result = await bitcoinApi.$getBlock(req.params.hash);
|
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||||
|
@ -17,7 +17,7 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=1100}
|
|||||||
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
|
||||||
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
|
||||||
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]}
|
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://mempool.space/resources/pools.json\"]}
|
||||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=debug}
|
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
|
@ -66,7 +66,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
|
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
|
||||||
cy.get('.footer').should('be.visible');
|
cy.get('.footer').should('be.visible');
|
||||||
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
|
||||||
expect(text).to.match(/Tx vBytes per second:.* vB\/s/);
|
expect(text).to.match(/Incoming transactions.* vB\/s/);
|
||||||
});
|
});
|
||||||
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
|
||||||
expect(text).to.match(/Unconfirmed:(.*)/);
|
expect(text).to.match(/Unconfirmed:(.*)/);
|
||||||
|
8507
frontend/package-lock.json
generated
@ -59,39 +59,38 @@
|
|||||||
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
|
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
|
||||||
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
|
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
|
||||||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||||
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "^13.1.2",
|
"@angular-devkit/build-angular": "^13.2.4",
|
||||||
"@angular/animations": "~13.1.1",
|
"@angular/animations": "~13.2.3",
|
||||||
"@angular/cli": "~13.0.4",
|
"@angular/cli": "~13.2.4",
|
||||||
"@angular/common": "~13.1.1",
|
"@angular/common": "~13.2.3",
|
||||||
"@angular/compiler": "~13.1.1",
|
"@angular/compiler": "~13.2.3",
|
||||||
"@angular/core": "~13.1.1",
|
"@angular/core": "~13.2.3",
|
||||||
"@angular/forms": "~13.1.1",
|
"@angular/forms": "~13.2.3",
|
||||||
"@angular/localize": "^13.1.1",
|
"@angular/localize": "^13.2.3",
|
||||||
"@angular/platform-browser": "~13.1.1",
|
"@angular/platform-browser": "~13.2.3",
|
||||||
"@angular/platform-browser-dynamic": "~13.1.1",
|
"@angular/platform-browser-dynamic": "~13.2.3",
|
||||||
"@angular/platform-server": "~13.1.1",
|
"@angular/platform-server": "~13.2.3",
|
||||||
"@angular/router": "~13.1.1",
|
"@angular/router": "~13.2.3",
|
||||||
"@fortawesome/angular-fontawesome": "^0.8.2",
|
"@fortawesome/angular-fontawesome": "0.10.1",
|
||||||
"@fortawesome/fontawesome-common-types": "^0.2.35",
|
"@fortawesome/fontawesome-common-types": "0.3.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
"@fortawesome/free-solid-svg-icons": "6.0.0",
|
||||||
"@juggle/resize-observer": "^3.3.1",
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"@mempool/mempool.js": "2.3.0",
|
"@mempool/mempool.js": "2.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@nguniversal/express-engine": "11.2.1",
|
"@nguniversal/express-engine": "12.1.3",
|
||||||
"@types/qrcode": "1.4.1",
|
"@types/qrcode": "1.4.1",
|
||||||
"bootstrap": "4.5.0",
|
"bootstrap": "4.5.0",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "^5.1.2",
|
"echarts": "5.3.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"lightweight-charts": "^3.3.0",
|
"lightweight-charts": "^3.3.0",
|
||||||
"ngx-bootrap-multiselect": "^2.0.0",
|
"ngx-bootrap-multiselect": "^2.0.0",
|
||||||
"ngx-echarts": "^7.0.1",
|
"ngx-echarts": "8.0.1",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.0",
|
||||||
"rxjs": "^6.6.7",
|
"rxjs": "^6.6.7",
|
||||||
@ -101,9 +100,9 @@
|
|||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "~13.1.1",
|
"@angular/compiler-cli": "~13.2.3",
|
||||||
"@angular/language-service": "~13.1.1",
|
"@angular/language-service": "~13.2.3",
|
||||||
"@nguniversal/builders": "^11.2.1",
|
"@nguniversal/builders": "~13.0.2",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
@ -123,10 +122,10 @@
|
|||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^1.3.0",
|
"@cypress/schematic": "^1.3.0",
|
||||||
"cypress": "^9.3.1",
|
"cypress": "^9.5.0",
|
||||||
"cypress-fail-on-console-error": "^2.1.3",
|
"cypress-fail-on-console-error": "^2.1.3",
|
||||||
"cypress-wait-until": "^1.7.1",
|
"cypress-wait-until": "^1.7.1",
|
||||||
"mock-socket": "^9.0.3",
|
"mock-socket": "^9.0.3",
|
||||||
"start-server-and-test": "^1.12.6"
|
"start-server-and-test": "^1.12.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -42,7 +42,28 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
|
|||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
"^/liquid/api/": "/api/v1/"
|
"^/liquid/api/": "/api/v1/"
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/v1/**'],
|
||||||
|
target: `http://localhost:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquidtestnet": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/**'],
|
||||||
|
target: `http://localhost:8999`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquidtestnet/api/": "/api/v1/"
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,24 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
proxyTimeout: 30000,
|
proxyTimeout: 30000,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/v1/**'],
|
||||||
|
target: `http://localhost:8999`,
|
||||||
|
secure: false,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/liquidtestnet": ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: ['/liquidtestnet/api/**'],
|
||||||
|
target: `https://liquid.network`,
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true,
|
||||||
|
proxyTimeout: 30000,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ import { AssetsFeaturedComponent } from './components/assets/assets-featured/ass
|
|||||||
import { AssetsComponent } from './components/assets/assets.component';
|
import { AssetsComponent } from './components/assets/assets.component';
|
||||||
import { PoolComponent } from './components/pool/pool.component';
|
import { PoolComponent } from './components/pool/pool.component';
|
||||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
|
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
|
||||||
|
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
|
||||||
|
import { MiningStartComponent } from './components/mining-start/mining-start.component';
|
||||||
|
|
||||||
let routes: Routes = [
|
let routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -70,16 +72,31 @@ let routes: Routes = [
|
|||||||
component: LatestBlocksComponent,
|
component: LatestBlocksComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/difficulty',
|
path: 'mining',
|
||||||
component: DifficultyChartComponent,
|
component: MiningStartComponent,
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: 'mining/pools',
|
path: 'hashrate',
|
||||||
component: PoolRankingComponent,
|
component: HashrateChartComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/pool/:poolId',
|
path: 'hashrate/pools',
|
||||||
component: PoolComponent,
|
component: HashrateChartPoolsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pools',
|
||||||
|
component: PoolRankingComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pool',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':poolId',
|
||||||
|
component: PoolComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'graphs',
|
path: 'graphs',
|
||||||
@ -170,16 +187,31 @@ let routes: Routes = [
|
|||||||
component: LatestBlocksComponent,
|
component: LatestBlocksComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/difficulty',
|
path: 'mining',
|
||||||
component: DifficultyChartComponent,
|
component: MiningStartComponent,
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: 'mining/pools',
|
path: 'hashrate',
|
||||||
component: PoolRankingComponent,
|
component: HashrateChartComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/pool/:poolId',
|
path: 'hashrate/pools',
|
||||||
component: PoolComponent,
|
component: HashrateChartPoolsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pools',
|
||||||
|
component: PoolRankingComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pool',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':poolId',
|
||||||
|
component: PoolComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'graphs',
|
path: 'graphs',
|
||||||
@ -264,16 +296,31 @@ let routes: Routes = [
|
|||||||
component: LatestBlocksComponent,
|
component: LatestBlocksComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/difficulty',
|
path: 'mining',
|
||||||
component: DifficultyChartComponent,
|
component: MiningStartComponent,
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: 'mining/pools',
|
path: 'hashrate',
|
||||||
component: PoolRankingComponent,
|
component: HashrateChartComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mining/pool/:poolId',
|
path: 'hashrate/pools',
|
||||||
component: PoolComponent,
|
component: HashrateChartPoolsComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pools',
|
||||||
|
component: PoolRankingComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pool',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':poolId',
|
||||||
|
component: PoolComponent,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'graphs',
|
path: 'graphs',
|
||||||
|
@ -71,7 +71,23 @@ export const chartColors = [
|
|||||||
"#263238",
|
"#263238",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
export const poolsColor = {
|
||||||
|
'foundryusa': '#D81B60',
|
||||||
|
'antpool': '#8E24AA',
|
||||||
|
'f2pool': '#5E35B1',
|
||||||
|
'poolin': '#3949AB',
|
||||||
|
'binancepool': '#1E88E5',
|
||||||
|
'viabtc': '#039BE5',
|
||||||
|
'btccom': '#00897B',
|
||||||
|
'slushpool': '#00ACC1',
|
||||||
|
'sbicrypto': '#43A047',
|
||||||
|
'marapool': '#7CB342',
|
||||||
|
'luxor': '#C0CA33',
|
||||||
|
'unknown': '#FDD835',
|
||||||
|
'okkong': '#FFB300',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||||
|
|
||||||
export interface Language {
|
export interface Language {
|
||||||
|
@ -68,8 +68,12 @@ import { PushTransactionComponent } from './components/push-transaction/push-tra
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||||
|
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
|
||||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||||
import { DifficultyChartComponent } from './components/difficulty-chart/difficulty-chart.component';
|
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
|
||||||
|
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
|
||||||
|
import { MiningStartComponent } from './components/mining-start/mining-start.component';
|
||||||
|
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -120,8 +124,12 @@ import { DifficultyChartComponent } from './components/difficulty-chart/difficul
|
|||||||
AssetsNavComponent,
|
AssetsNavComponent,
|
||||||
AssetsFeaturedComponent,
|
AssetsFeaturedComponent,
|
||||||
AssetGroupComponent,
|
AssetGroupComponent,
|
||||||
|
AssetCirculationComponent,
|
||||||
MiningDashboardComponent,
|
MiningDashboardComponent,
|
||||||
DifficultyChartComponent,
|
HashrateChartComponent,
|
||||||
|
HashrateChartPoolsComponent,
|
||||||
|
MiningStartComponent,
|
||||||
|
AmountShortenerPipe,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||||
|
@ -69,7 +69,7 @@ export function calcSegwitFeeGains(tx: Transaction) {
|
|||||||
export function moveDec(num: number, n: number) {
|
export function moveDec(num: number, n: number) {
|
||||||
let frac, int, neg, ref;
|
let frac, int, neg, ref;
|
||||||
if (n === 0) {
|
if (n === 0) {
|
||||||
return num;
|
return num.toString();
|
||||||
}
|
}
|
||||||
ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
|
ref = ('' + num).split('.'), int = ref[0], frac = ref[1];
|
||||||
int || (int = '0');
|
int || (int = '0');
|
||||||
@ -130,3 +130,32 @@ export const formatNumber = (s, precision = null) => {
|
|||||||
// Utilities for segwitFeeGains
|
// Utilities for segwitFeeGains
|
||||||
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
|
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
|
||||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
||||||
|
|
||||||
|
// Power of ten wrapper
|
||||||
|
export function selectPowerOfTen(val: number) {
|
||||||
|
const powerOfTen = {
|
||||||
|
exa: Math.pow(10, 18),
|
||||||
|
peta: Math.pow(10, 15),
|
||||||
|
terra: Math.pow(10, 12),
|
||||||
|
giga: Math.pow(10, 9),
|
||||||
|
mega: Math.pow(10, 6),
|
||||||
|
kilo: Math.pow(10, 3),
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectedPowerOfTen;
|
||||||
|
if (val < powerOfTen.mega) {
|
||||||
|
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||||
|
} else if (val < powerOfTen.giga) {
|
||||||
|
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||||
|
} else if (val < powerOfTen.terra) {
|
||||||
|
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||||
|
} else if (val < powerOfTen.peta) {
|
||||||
|
selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
|
||||||
|
} else if (val < powerOfTen.exa) {
|
||||||
|
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
||||||
|
} else {
|
||||||
|
selectedPowerOfTen = { divider: powerOfTen.exa, unit: 'E' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedPowerOfTen;
|
||||||
|
}
|
@ -8,13 +8,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="about-text" *ngIf="stateService.env.BASE_MODULE === 'mempool'; else marginBox">
|
<div class="about-text">
|
||||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||||
<p i18n>Building a mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, without any advertising, altcoins, or third-party trackers.</p>
|
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #marginBox>
|
|
||||||
<div class="no-about-margin"></div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<div class="social-icons">
|
<div class="social-icons">
|
||||||
<a target="_blank" href="https://github.com/mempool/mempool">
|
<a target="_blank" href="https://github.com/mempool/mempool">
|
||||||
@ -31,31 +28,128 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
<div class="sponsor-button">
|
||||||
|
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
|
||||||
|
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="enterprise-sponsor">
|
<div class="enterprise-sponsor">
|
||||||
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<a href="https://spiral.xyz/" target="_blank" title="Spiral">
|
<a href="https://spiral.xyz/" target="_blank" title="Spiral">
|
||||||
<img class="image" src="/resources/profile/spiral.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-115 -15 879 679" style="background-color: rgb(27,20,100)" class="image">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:url(#linear-gradient);}</style>
|
||||||
|
<linearGradient id="linear-gradient" x1="81.36" y1="311.35" x2="541.35" y2="311.35" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.18" stop-color="blue"/>
|
||||||
|
<stop offset="1" stop-color="#f0f"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-1" d="M326.4,572.09C201.2,572.09,141,503,112.48,445,84.26,387.47,81.89,330.44,81.69,322.31c-4.85-77,41-231.78,249.58-271.2a28.05,28.05,0,0,1,10.41,55.13c-213.12,40.28-204.44,206-204,213,0,.53.06,1.06.07,1.6C137.9,328.74,142.85,516,326.4,516,394.74,516,443,486.6,470,428.63c24.48-52.74,19.29-112.45-13.52-155.83-22.89-30.27-52.46-45-90.38-45-34.46,0-63.47,9.88-86.21,29.37A91.5,91.5,0,0,0,248,322.3c-1.41,25.4,7.14,49.36,24.07,67.49C287.27,406,305,413.9,326.4,413.9c27.46,0,45.52-9,53.66-26.81,8.38-18.3,3.61-38.93-.19-43.33-9.11-10-18.69-13.68-22.48-13-2.53.43-5.78,4.61-8.48,10.92a28,28,0,0,1-51.58-22c14.28-33.44,37.94-42,50.76-44.2,24.78-4.18,52.17,7.3,73.34,30.65s25.51,68.55,10.15,103.22C421.54,432,394.52,470,326.4,470c-36.72,0-69.67-14.49-95.29-41.92C203.64,398.68,189.77,360,192,319.19a149.1,149.1,0,0,1,51.31-104.6c33.19-28.45,74.48-42.87,122.71-42.87,55.12,0,101.85,23.25,135.12,67.23,45.36,60,52.9,141.71,19.66,213.3C495.45,506.92,441.12,572.09,326.4,572.09Z"/>
|
||||||
|
</svg>
|
||||||
<span>Spiral</span>
|
<span>Spiral</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||||
<img class="image" src="/resources/profile/gemini.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||||
|
<rect style="fill: white" width="360" height="360" />
|
||||||
|
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||||
|
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
<span>Gemini</span>
|
<span>Gemini</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||||
<img class="image" src="/resources/profile/exodus.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
|
||||||
|
<defs>
|
||||||
|
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
|
||||||
|
<stop stop-color="#00BFFF" offset="0%"></stop>
|
||||||
|
<stop stop-color="#6619FF" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g>
|
||||||
|
<rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
|
||||||
|
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||||
<img class="image" src="/resources/profile/foundry.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
|
||||||
|
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g>
|
||||||
|
<rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
|
||||||
|
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
<span>Foundry</span>
|
<span>Foundry</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||||
<img class="image" src="/resources/profile/unchained.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
|
||||||
|
<style type="text/css">
|
||||||
|
.ucst0{fill:#002248;}
|
||||||
|
.ucst1{opacity:0.5;fill:#FFFFFF;}
|
||||||
|
.ucst2{fill:#FFFFFF;}
|
||||||
|
.ucst3{opacity:0.75;fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<rect class="ucst0" width="216" height="216"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
|
||||||
|
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
|
||||||
|
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
<span>Unchained</span>
|
<span>Unchained</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://blockstream.com/" target="_blank" title="Blockstream">
|
<a href="https://blockstream.com/" target="_blank" title="Blockstream">
|
||||||
<img class="image" src="/resources/profile/blockstream.svg" />
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#111316;}
|
||||||
|
.st1{fill:#00C3FF;}
|
||||||
|
.st2{fill:#7EE0FF;}
|
||||||
|
</style>
|
||||||
|
<path class="st1" d="M659.7,392.3c10.2,14.3,18.4,29.9,24.5,46.4l21.8-7.1c-6.9-18.9-16.4-36.8-28.1-53.1L659.7,392.3z"/>
|
||||||
|
<path class="st1" d="M510.6,289.2c-5.8-0.2-11.7-0.2-17.5,0l1.6,22.8c8.8-0.3,17.6-0.1,26.3,0.7c8.7,0.8,17.4,2.2,26,4.2l5.8-22.1 c-9.8-2.3-19.7-3.9-29.7-4.8C519,289.6,514.7,289.3,510.6,289.2z"/>
|
||||||
|
<path class="st1" d="M297.1,605.5c-9.1-18.6-15.7-38.3-19.5-58.6l-23.9,3.8c4.2,23,11.6,45.3,22,66.2L297.1,605.5z"/>
|
||||||
|
<path class="st1" d="M284.8,375.6l21.2,11.8c10.6-17.8,23.5-34,38.5-48.3l-16.2-18C311.3,337.2,296.7,355.5,284.8,375.6z"/>
|
||||||
|
<path class="st1" d="M254.8,453.5l23.8,4.2c4.2-20.3,11.2-39.9,20.7-58.3l-21.2-11.7C267.3,408.5,259.5,430.6,254.8,453.5z"/>
|
||||||
|
<path class="st1" d="M409.9,268.8l9.5,22.2c19.3-7.6,39.5-12.5,60.1-14.5l-1.7-24.1C454.5,254.6,431.7,260.1,409.9,268.8z"/>
|
||||||
|
<path class="st1" d="M338.5,311.8l16.2,18c15.8-13.4,33.3-24.6,52.1-33.4l-9.5-22.2C376,283.9,356.2,296.6,338.5,311.8z"/>
|
||||||
|
<path class="st1" d="M697.1,667.6l-18.9-15.1c-13.4,15.8-28.9,29.7-46,41.4l13,20.5C664.6,701.3,682.1,685.6,697.1,667.6z"/>
|
||||||
|
<path class="st1" d="M402.5,710.7c-18.6-9.1-35.9-20.7-51.4-34.5l-16.5,17.7c17.4,15.6,37,28.6,58,38.8L402.5,710.7z"/>
|
||||||
|
<path class="st1" d="M755.4,528.2c3.1-32.6-0.2-65.5-9.7-96.8l-23,7.6c13.2,44.4,12.7,91.7-1.3,135.8l22.8,8.1 C749.9,565.2,753.7,546.8,755.4,528.2z"/>
|
||||||
|
<path class="st1" d="M614.2,689.2L602,670c-15.1,9-31.3,16-48.3,20.7l5.4,22.2C578.5,707.5,597,699.6,614.2,689.2z"/>
|
||||||
|
<path class="st1" d="M314.5,528.8c-1.7-14.2-1.9-28.6-0.5-42.9c0.3-3.5,0.7-6.5,1.2-9.6l-22.5-4c-0.5,3.8-1,7.6-1.4,11.5 c-1.5,16.1-1.3,32.4,0.7,48.5L314.5,528.8z"/>
|
||||||
|
<path class="st1" d="M568.2,284.7c19.9,5.8,38.9,14.4,56.4,25.4l13.5-20.2c-19.8-12.5-41.2-22.1-63.7-28.7L568.2,284.7z"/>
|
||||||
|
<path class="st1" d="M469.8,755.8l2.3-24.1c-19.5-2.6-38.6-7.8-56.8-15.3l-10.1,22.2C425.8,747.1,447.6,752.9,469.8,755.8z"/>
|
||||||
|
<path class="st1" d="M351.3,657.7l15.7-16.6c-12.4-12.5-23.1-26.5-31.8-41.8l-20.3,10.7C324.8,627.4,337.1,643.5,351.3,657.7z"/>
|
||||||
|
<path class="st1" d="M649.5,297.7l-13.6,20.2c16.9,12,32,26.3,45.1,42.4l19.4-14.8C685.7,327.2,668.6,311.2,649.5,297.7z"/>
|
||||||
|
<path class="st1" d="M672.7,633.2c12-16.1,21.8-33.7,29.1-52.5l-21.5-7.7c-6.4,16.4-15,31.9-25.5,46L672.7,633.2z"/>
|
||||||
|
<path class="st2" d="M690.6,449.6l-21.6,7.2c6,20.7,8,42.4,6,63.8c-1.1,11.9-3.4,23.7-6.9,35.2l21.5,7.6c4.1-13.2,6.9-26.9,8.2-40.7 C700.1,498.1,697.6,473.3,690.6,449.6z"/>
|
||||||
|
<path class="st2" d="M475.2,698l2.1-22.7c-13.3-2-26.4-5.5-38.9-10.5l-9.4,20.7C443.8,691.5,459.3,695.7,475.2,698z"/>
|
||||||
|
<path class="st2" d="M631.8,456.2l20.4-6.9c-4.9-12.9-11.4-25.2-19.4-36.6l-17.1,13C622.3,435.2,627.7,445.4,631.8,456.2z"/>
|
||||||
|
<path class="st2" d="M508.4,345.7h-11.2l1.5,21.4c11.5-0.3,22.9,0.7,34.2,3.2l5.5-20.7c-6.8-1.5-13.6-2.6-20.5-3.2 C514.8,346.1,511.6,345.9,508.4,345.7z"/>
|
||||||
|
<path class="st2" d="M335.5,403.8l20,11.1c7.5-12.4,16.5-23.7,26.9-33.8L367,364.1C354.8,375.9,344.2,389.2,335.5,403.8z"/>
|
||||||
|
<path class="st2" d="M553.8,339.5c13.8,4.2,27.1,10.2,39.4,17.7l12.7-19c-14.4-8.9-30-15.8-46.2-20.7L553.8,339.5z"/>
|
||||||
|
<path class="st2" d="M635.9,394.5l18.1-13.8c-10.7-13.2-23.2-24.9-36.9-34.8l-12.7,19C616.2,373.4,626.7,383.3,635.9,394.5z"/>
|
||||||
|
<path class="st2" d="M611.5,584.6l16.8,13.4c8.2-11.2,14.9-23.3,20.1-36.2l-20.2-7.2C623.8,565.2,618.2,575.3,611.5,584.6z"/>
|
||||||
|
<path class="st2" d="M389.9,635.1l-15.6,16.6c12.8,11.2,26.9,20.7,42.2,28.2l9.4-20.7C412.9,652.8,400.8,644.6,389.9,635.1z"/>
|
||||||
|
<path class="st2" d="M369.2,520.2c-1-9.7-1.1-19.5-0.2-29.2c0.2-1.7,0.4-3.5,0.6-5.1l-21.1-3.8c-0.3,2.3-0.6,4.6-0.8,6.9 c-1.1,11.5-0.9,23,0.3,34.5L369.2,520.2z"/>
|
||||||
|
<path class="st2" d="M333.6,538l-22.6,3.5c3.2,16.7,8.6,33,16,48.3l20.2-10.7C340.9,566,336.4,552.2,333.6,538z"/>
|
||||||
|
<path class="st2" d="M601.7,646.3l12.3,19.2c14-9.6,26.7-21,37.7-33.8l-17.9-14.2C624.4,628.4,613.6,638.1,601.7,646.3z"/>
|
||||||
|
<path class="st2" d="M348.8,426.9l-19.9-11c-7.8,15.1-13.5,31.2-17,47.8l22.5,4C337.4,453.5,342.2,439.8,348.8,426.9z"/>
|
||||||
|
<path class="st2" d="M540.6,636.9l5,20.7c13.3-3.8,26.1-9.2,38.1-16.2l-11.6-18.1C562.2,629,551.6,633.6,540.6,636.9z"/>
|
||||||
|
<path class="st2" d="M384,573.5l-19,9.9c6.9,12,15.4,23,25.1,32.9l14.8-15.7C396.9,592.4,389.9,583.3,384,573.5z"/>
|
||||||
|
<path class="st2" d="M496.7,677.1c-1.9,0-3.8-0.2-5.7-0.4l-2.1,22.7c17.9,1.3,35.9,0.1,53.4-3.5l-5.3-22.2 C523.8,676.5,510.2,677.6,496.7,677.1z"/>
|
||||||
|
<path class="st2" d="M377.3,354.9l15.3,16.9c11.1-9.3,23.3-17.1,36.4-23.3l-9-21C404.6,334.7,390.3,343.9,377.3,354.9z"/>
|
||||||
|
<path class="st2" d="M432.7,322.1l9,21c13.5-5.2,27.6-8.7,42-10.3L482,310C465.1,311.9,448.5,315.9,432.7,322.1z"/>
|
||||||
|
<path class="st1" d="M490.3,757.5c21.5,0.7,43-1.1,64.2-5.2l-5-23.3c-18.3,3.8-37,5.3-55.8,4.6c-3,0-5.2-0.4-8.2-0.6l-2.1,24.4 c2.3,0.1,4.6,0.1,6.9,0L490.3,757.5z"/>
|
||||||
|
</svg>
|
||||||
<span>Blockstream</span>
|
<span>Blockstream</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -73,9 +167,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
|
|
||||||
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="community-integrations-sponsor">
|
<div class="community-integrations-sponsor">
|
||||||
@ -106,6 +197,10 @@
|
|||||||
<img class="image" src="/resources/profile/runcitadel.svg" />
|
<img class="image" src="/resources/profile/runcitadel.svg" />
|
||||||
<span>Citadel</span>
|
<span>Citadel</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/fort-nix/nix-bitcoin" target="_blank" title="nix-bitcoin">
|
||||||
|
<img class="image" src="/resources/profile/nix-bitcoin.png" />
|
||||||
|
<span>NixOS</span>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
|
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
|
||||||
<img class="image" src="/resources/profile/electrum.jpg" />
|
<img class="image" src="/resources/profile/electrum.jpg" />
|
||||||
<span>Electrum</span>
|
<span>Electrum</span>
|
||||||
@ -142,10 +237,6 @@
|
|||||||
<img class="image" src="/resources/profile/marina.svg" />
|
<img class="image" src="/resources/profile/marina.svg" />
|
||||||
<span>Marina</span>
|
<span>Marina</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/Satpile/satpile" target="_blank" title="Satpile Watch-Only Wallet">
|
|
||||||
<img class="image" src="/resources/profile/satpile.jpg" />
|
|
||||||
<span>Satpile</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -177,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="allContributors$ | async as contributors else loadingSponsors">
|
<ng-container *ngIf="allContributors$ | async as contributors else loadingSponsors">
|
||||||
<div class="contributors">
|
<div class="contributors">
|
||||||
<h3 i18n="about.contributors">Project Contributors</h3>
|
<h3 i18n="about.contributors">Project Contributors</h3>
|
||||||
@ -252,7 +343,7 @@
|
|||||||
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
<a href="/3rdpartylicenses.txt">Third-party Licenses</a>
|
||||||
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-version" *ngIf="officialMempoolSpace">
|
<div class="footer-version" *ngIf="officialMempoolSpace">
|
||||||
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
|
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 25px;
|
margin: 25px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
margin: 25px auto 30px;
|
margin: 25px auto 30px;
|
||||||
@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alliances,
|
.alliances,
|
||||||
.enterprise-sponsor,
|
.enterprise-sponsor,
|
||||||
.community-integrations-sponsor,
|
.community-integrations-sponsor,
|
||||||
.maintainers {
|
.maintainers {
|
||||||
margin-top: 68px;
|
margin-top: 68px;
|
||||||
@ -58,7 +58,7 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
.alliances {
|
.alliances {
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
a {
|
a {
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@ -88,17 +88,17 @@
|
|||||||
margin: 50px 30px 0px;
|
margin: 50px 30px 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.liquid {
|
.liquid {
|
||||||
top: 7px;
|
top: 7px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.copa {
|
.copa {
|
||||||
height: auto;
|
height: auto;
|
||||||
top: 23px;
|
top: 23px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
.bisq {
|
.bisq {
|
||||||
top: 3px;
|
top: 3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -115,15 +115,15 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
img {
|
img, svg {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
img, span{
|
img, svg, span {
|
||||||
display: block;
|
display: block;
|
||||||
transition: 150ms all;
|
transition: 150ms all;
|
||||||
}
|
}
|
||||||
img {
|
img, svg {
|
||||||
margin: 40px 29px 10px;
|
margin: 40px 29px 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,53 +46,68 @@ export class AddressLabelsComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||||
// {regexp: /^OP_DUP OP_HASH160/, label: 'HTLC'},
|
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSHBYTES_(1 \w{2}|2 \w{4}) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
|
||||||
{regexp: /^OP_IF OP_PUSHBYTES_33 \w{33} OP_ELSE OP_PUSHBYTES_2 \w{2} OP_CSV OP_DROP/, label: 'Force Close'}
|
if (this.vin.witness[this.vin.witness.length - 2] == '01') {
|
||||||
].forEach((item) => {
|
this.lightning = 'Revoked Force Close';
|
||||||
if (item.regexp.test(this.vin.inner_witnessscript_asm)) {
|
} else {
|
||||||
this.lightning = item.label;
|
this.lightning = 'Force Close';
|
||||||
}
|
}
|
||||||
|
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||||
|
} else if (/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CHECKSEQUENCEVERIFY OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||||
|
if (this.vin.witness[this.vin.witness.length - 2].length == 66) {
|
||||||
|
this.lightning = 'Revoked HTLC';
|
||||||
|
} else {
|
||||||
|
this.lightning = 'HTLC';
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
if (this.lightning) {
|
if (this.lightning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
|
this.detectMultisig(this.vin.inner_witnessscript_asm);
|
||||||
const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
|
}
|
||||||
this.multisig = true;
|
|
||||||
this.multisigM = parseInt(matches[0], 10);
|
|
||||||
this.multisigN = parseInt(matches[1], 10);
|
|
||||||
|
|
||||||
if (this.multisigM === 1 && this.multisigN === 1) {
|
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
||||||
this.multisig = false;
|
}
|
||||||
}
|
|
||||||
|
detectMultisig(script: string) {
|
||||||
|
if (!script) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ops = script.split(' ');
|
||||||
|
if (ops.length < 3 || ops.pop() != 'OP_CHECKMULTISIG') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const opN = ops.pop();
|
||||||
|
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = parseInt(opN.match(/[0-9]+/)[0]);
|
||||||
|
if (ops.length < n * 2 + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// pop n public keys
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const opM = ops.pop();
|
||||||
if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
|
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||||
const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
|
return;
|
||||||
this.multisig = true;
|
|
||||||
this.multisigM = matches[0];
|
|
||||||
this.multisigN = matches[1];
|
|
||||||
}
|
}
|
||||||
|
const m = parseInt(opM.match(/[0-9]+/)[0]);
|
||||||
|
|
||||||
|
this.multisig = true;
|
||||||
|
this.multisigM = m;
|
||||||
|
this.multisigN = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVout() {
|
handleVout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatches(str: string, regex: RegExp, index: number) {
|
|
||||||
if (!index) {
|
|
||||||
index = 1;
|
|
||||||
}
|
|
||||||
const matches = [];
|
|
||||||
let match;
|
|
||||||
while (match = regex.exec(str)) {
|
|
||||||
matches.push(match[index]);
|
|
||||||
}
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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> <span class="ticker">{{ circulating.ticker }}</span></ng-template>
|
||||||
|
</ng-container>
|
@ -0,0 +1,3 @@
|
|||||||
|
.ticker {
|
||||||
|
color: grey;
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,10 @@
|
|||||||
<div class="fee-span">
|
<div class="fee-span">
|
||||||
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-size" [innerHTML]="'‎' + (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]="'‎' + (block.size | bytes: 2)"></div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||||
@ -21,10 +24,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
<div class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="" *ngIf="showMiningInfo === true">
|
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||||
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
|
<a class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.id) | relativeUrl]">
|
||||||
{{ block.extras.pool.name}}</a>
|
{{ block.extras.pool.name}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||||
|
@ -129,4 +129,15 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animated {
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events : none;
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { specialBlocks } from 'src/app/app.constants';
|
import { specialBlocks } from 'src/app/app.constants';
|
||||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { config } from 'process';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blockchain-blocks',
|
selector: 'app-blockchain-blocks',
|
||||||
@ -12,7 +13,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||||
@Input() showMiningInfo: boolean = false;
|
|
||||||
specialBlocks = specialBlocks;
|
specialBlocks = specialBlocks;
|
||||||
network = '';
|
network = '';
|
||||||
blocks: BlockExtended[] = [];
|
blocks: BlockExtended[] = [];
|
||||||
@ -32,6 +32,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
arrowLeftPx = 30;
|
arrowLeftPx = 30;
|
||||||
blocksFilled = false;
|
blocksFilled = false;
|
||||||
transition = '1s';
|
transition = '1s';
|
||||||
|
showMiningInfo = false;
|
||||||
|
|
||||||
gradientColors = {
|
gradientColors = {
|
||||||
'': ['#9339f4', '#105fb0'],
|
'': ['#9339f4', '#105fb0'],
|
||||||
@ -44,11 +45,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private router: Router,
|
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
) { }
|
private location: Location,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledMiningInfoIfNeeded(url) {
|
||||||
|
this.showMiningInfo = url === '/mining';
|
||||||
|
this.cd.markForCheck(); // Need to update the view asap
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
|
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||||
|
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||||
|
}
|
||||||
|
|
||||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||||
this.feeRounding = '1.0-1';
|
this.feeRounding = '1.0-1';
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<div class="text-center" class="blockchain-wrapper animate" #container>
|
<div class="text-center" class="blockchain-wrapper" #container>
|
||||||
<div class="position-container {{ network }}">
|
<div class="position-container {{ network }}">
|
||||||
<span>
|
<span>
|
||||||
<app-mempool-blocks></app-mempool-blocks>
|
<app-mempool-blocks></app-mempool-blocks>
|
||||||
<app-blockchain-blocks [showMiningInfo]="showMiningInfo"></app-blockchain-blocks>
|
<app-blockchain-blocks></app-blockchain-blocks>
|
||||||
<div id="divider"></div>
|
<div id="divider"></div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,14 +59,4 @@
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
left: -150px;
|
left: -150px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate {
|
|
||||||
transition: all 1s ease-in-out;
|
|
||||||
}
|
|
||||||
.move-left {
|
|
||||||
transform: translate(-40%, 0);
|
|
||||||
@media (max-width: 767.98px) {
|
|
||||||
transform: translate(-85%, 0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockchainComponent implements OnInit {
|
export class BlockchainComponent implements OnInit {
|
||||||
showMiningInfo: boolean = false;
|
|
||||||
network: string;
|
network: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -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>‎{{ 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>
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,15 +2,18 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
<div class="row text-center" *ngIf="mempoolInfoData$ | async as mempoolInfoData">
|
||||||
<div class="col d-none d-sm-block">
|
<div class="col d-none d-sm-block">
|
||||||
<span class="txPerSecond" i18n="footer.tx-vbytes-per-second">Tx vBytes per second:</span>
|
<span class="txPerSecond" i18n="dashboard.incoming-transactions">Incoming transactions</span>
|
||||||
<span *ngIf="mempoolInfoData.vBytesPerSecond === 0; else inSync">
|
<ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData" [ngIfElse]="loadingTransactions">
|
||||||
<span class="badge badge-pill badge-warning" i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</span>
|
<span *ngIf="(mempoolLoadingStatus$ | async) !== 100; else inSync">
|
||||||
</span>
|
<span class="badge badge-pill badge-warning"><ng-container i18n="dashboard.backend-is-synchronizing">Backend is synchronizing</ng-container> ({{ mempoolLoadingStatus$ | async }}%)</span>
|
||||||
<ng-template #inSync>
|
</span>
|
||||||
<div class="progress sub-text">
|
<ng-template #inSync>
|
||||||
<div class="progress-bar {{ mempoolInfoData.progressClass }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth}">{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
|
<div class="progress inc-tx-progress-bar">
|
||||||
</div>
|
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.progressWidth, 'background-color': mempoolInfoData.progressColor}"> </div>
|
||||||
</ng-template>
|
<div class="progress-text">‎{{ mempoolInfoData.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span>
|
<span class="unconfirmedTx"><ng-container i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</ng-container>:</span>
|
||||||
@ -25,3 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<ng-template #loadingTransactions>
|
||||||
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
|
</ng-template>
|
@ -13,7 +13,7 @@ interface MempoolInfoData {
|
|||||||
memPoolInfo: MempoolInfo;
|
memPoolInfo: MempoolInfo;
|
||||||
vBytesPerSecond: number;
|
vBytesPerSecond: number;
|
||||||
progressWidth: string;
|
progressWidth: string;
|
||||||
progressClass: string;
|
progressColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -26,35 +26,62 @@ export class FooterComponent implements OnInit {
|
|||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
vBytesPerSecondLimit = 1667;
|
vBytesPerSecondLimit = 1667;
|
||||||
|
isLoadingWebSocket$: Observable<boolean>;
|
||||||
|
mempoolLoadingStatus$: Observable<number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.mempoolInfoData$ = combineLatest([
|
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.stateService.mempoolInfo$,
|
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||||
this.stateService.vbytesPerSecond$
|
.pipe(
|
||||||
])
|
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
|
||||||
.pipe(
|
);
|
||||||
map(([mempoolInfo, vbytesPerSecond]) => {
|
|
||||||
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
|
|
||||||
|
|
||||||
let progressClass = 'bg-danger';
|
this.mempoolInfoData$ = combineLatest([
|
||||||
if (percent <= 75) {
|
this.stateService.mempoolInfo$,
|
||||||
progressClass = 'bg-success';
|
this.stateService.vbytesPerSecond$
|
||||||
} else if (percent <= 99) {
|
])
|
||||||
progressClass = 'bg-warning';
|
.pipe(
|
||||||
}
|
map(([mempoolInfo, vbytesPerSecond]) => {
|
||||||
|
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
|
||||||
return {
|
|
||||||
memPoolInfo: mempoolInfo,
|
let progressColor = '#7CB342';
|
||||||
vBytesPerSecond: vbytesPerSecond,
|
if (vbytesPerSecond > 1667) {
|
||||||
progressWidth: percent + '%',
|
progressColor = '#FDD835';
|
||||||
progressClass: progressClass,
|
}
|
||||||
};
|
if (vbytesPerSecond > 2000) {
|
||||||
})
|
progressColor = '#FFB300';
|
||||||
);
|
}
|
||||||
|
if (vbytesPerSecond > 2500) {
|
||||||
|
progressColor = '#FB8C00';
|
||||||
|
}
|
||||||
|
if (vbytesPerSecond > 3000) {
|
||||||
|
progressColor = '#F4511E';
|
||||||
|
}
|
||||||
|
if (vbytesPerSecond > 3500) {
|
||||||
|
progressColor = '#D81B60';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
|
||||||
|
let mempoolSizeProgress = 'bg-danger';
|
||||||
|
if (mempoolSizePercentage <= 50) {
|
||||||
|
mempoolSizeProgress = 'bg-success';
|
||||||
|
} else if (mempoolSizePercentage <= 75) {
|
||||||
|
mempoolSizeProgress = 'bg-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
memPoolInfo: mempoolInfo,
|
||||||
|
vBytesPerSecond: vbytesPerSecond,
|
||||||
|
progressWidth: percent + '%',
|
||||||
|
progressColor: progressColor,
|
||||||
|
mempoolSizeProgress: mempoolSizeProgress,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
|
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -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">‎{{ 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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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%;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -160,6 +160,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: false,
|
smooth: false,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
|
symbol: 'none',
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 3,
|
width: 3,
|
||||||
},
|
},
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
|
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
|
||||||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" routerLinkActive="active" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
|
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
|
||||||
|
@ -12,7 +12,10 @@
|
|||||||
<div class="fee-span">
|
<div class="fee-span">
|
||||||
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ projectedBlock.feeRange[0] | number:feeRounding }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | number:feeRounding }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-size" [innerHTML]="'‎' + (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]="'‎' + (projectedBlock.blockSize | bytes: 2)"></div>
|
||||||
<div class="transaction-count">
|
<div class="transaction-count">
|
||||||
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
|
<ng-container *ngTemplateOutlet="projectedBlock.nTx === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: projectedBlock.nTx | number}"></ng-container>
|
||||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||||
|
@ -7,6 +7,7 @@ import { take, map, switchMap } from 'rxjs/operators';
|
|||||||
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
|
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
|
||||||
import { specialBlocks } from 'src/app/app.constants';
|
import { specialBlocks } from 'src/app/app.constants';
|
||||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-mempool-blocks',
|
selector: 'app-mempool-blocks',
|
||||||
@ -32,6 +33,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
network = '';
|
network = '';
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
|
showMiningInfo = false;
|
||||||
|
|
||||||
blockWidth = 125;
|
blockWidth = 125;
|
||||||
blockPadding = 30;
|
blockPadding = 30;
|
||||||
@ -54,9 +56,20 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
|
private location: Location
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
enabledMiningInfoIfNeeded(url) {
|
||||||
|
this.showMiningInfo = url === '/mining';
|
||||||
|
this.cd.markForCheck(); // Need to update the view asap
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
|
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||||
|
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||||
|
}
|
||||||
|
|
||||||
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
||||||
this.feeRounding = '1.0-1';
|
this.feeRounding = '1.0-1';
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,37 @@
|
|||||||
<div class="container-xl dashboard-container">
|
<div class="container-xl dashboard-container">
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
|
|
||||||
<!-- pool distribution -->
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="main-title" i18n="mining.pool-share">Mining Pools Share (1w)</div>
|
<div class="card double">
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<!-- pool distribution -->
|
||||||
|
<h5 class="card-title">
|
||||||
|
<a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="mining.pool-share">
|
||||||
|
Mining Pools Share (1w)
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
<app-pool-ranking [widget]=true></app-pool-ranking>
|
<app-pool-ranking [widget]=true></app-pool-ranking>
|
||||||
<div class="text-center"><a href="" [routerLink]="['/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
|
|
||||||
»</a></div>
|
<!-- pools hashrate -->
|
||||||
|
<app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- difficulty -->
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="main-title" i18n="mining.difficulty">Difficulty (1y)</div>
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<app-difficulty-chart [widget]=true></app-difficulty-chart>
|
<!-- hashrate -->
|
||||||
<div class="text-center"><a href="" [routerLink]="['/mining/difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
|
<h5 class="card-title">
|
||||||
»</a></div>
|
<a class="link" href="" [routerLink]="['/mining/hashrate' | relativeUrl]" i18n="mining.hashrate">
|
||||||
|
Hashrate (1y)
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<app-hashrate-chart [widget]=true></app-hashrate-chart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -12,21 +12,24 @@
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #1d1f31;
|
background-color: #1d1f31;
|
||||||
height: 100%;
|
height: 340px;
|
||||||
|
}
|
||||||
|
.card.double {
|
||||||
|
height: 620px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-wrapper {
|
.card-title {
|
||||||
.card {
|
font-size: 1rem;
|
||||||
height: auto !important;
|
}
|
||||||
}
|
.card-title > a {
|
||||||
.card-body {
|
color: #4a68b9;
|
||||||
display: flex;
|
}
|
||||||
flex: inherit;
|
|
||||||
text-align: center;
|
.card-body {
|
||||||
flex-direction: column;
|
padding: 1.25rem 1rem 0.75rem 1rem;
|
||||||
justify-content: space-around;
|
}
|
||||||
padding: 22px 20px;
|
.card-body.pool-ranking {
|
||||||
}
|
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blockchain-container {
|
#blockchain-container {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-mining-dashboard',
|
selector: 'app-mining-dashboard',
|
||||||
@ -8,7 +9,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class MiningDashboardComponent implements OnInit {
|
export class MiningDashboardComponent implements OnInit {
|
||||||
|
|
||||||
constructor() { }
|
constructor(private seoService: SeoService) {
|
||||||
|
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
<div [class]="widget === false ? 'container-xl' : ''">
|
<div [class]="widget === false ? 'container-xl' : ''">
|
||||||
|
|
||||||
<div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
<div [class]="widget ? 'chart-widget' : 'chart'"
|
||||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||||
|
<div class="text-center loadingGraphs" [class]="widget ? 'widget' : ''" *ngIf="isLoading">
|
||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-header mb-0 mb-lg-4" [style]="widget === true ? 'display:none' : ''">
|
<div class="card-header mb-0 mb-lg-4 mt-md-3" [style]="widget ? 'display:none' : ''">
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(miningStatsObservable$ | async) as miningStats">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">
|
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="miningStats.availableTimespanDay >= 1">
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
.hashrate-pie {
|
.chart {
|
||||||
height: 100%;
|
max-height: 400px;
|
||||||
min-height: 400px;
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
min-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chart-widget {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 275px;
|
||||||
|
}
|
||||||
|
|
||||||
.formRadioGroup {
|
.formRadioGroup {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
@ -30,3 +34,13 @@
|
|||||||
padding: .3em !important;
|
padding: .3em !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadingGraphs {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: calc(50% - 15px);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.loadingGraphs.widget {
|
||||||
|
top: 25%;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||||
@ -9,20 +9,13 @@ import { SeoService } from 'src/app/services/seo.service';
|
|||||||
import { StorageService } from '../..//services/storage.service';
|
import { StorageService } from '../..//services/storage.service';
|
||||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { chartColors } from 'src/app/app.constants';
|
import { chartColors, poolsColor } from 'src/app/app.constants';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pool-ranking',
|
selector: 'app-pool-ranking',
|
||||||
templateUrl: './pool-ranking.component.html',
|
templateUrl: './pool-ranking.component.html',
|
||||||
styleUrls: ['./pool-ranking.component.scss'],
|
styleUrls: ['./pool-ranking.component.scss'],
|
||||||
styles: [`
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
.loadingGraphs {
|
|
||||||
position: absolute;
|
|
||||||
top: 38%;
|
|
||||||
left: calc(50% - 15px);
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
`],
|
|
||||||
})
|
})
|
||||||
export class PoolRankingComponent implements OnInit {
|
export class PoolRankingComponent implements OnInit {
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
@ -33,7 +26,9 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
chartInitOptions = {
|
chartInitOptions = {
|
||||||
renderer: 'svg'
|
renderer: 'svg',
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
};
|
};
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
@ -47,13 +42,13 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) {
|
) {
|
||||||
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.widget) {
|
if (this.widget) {
|
||||||
this.poolsWindowPreference = '1w';
|
this.poolsWindowPreference = '1w';
|
||||||
} else {
|
} else {
|
||||||
|
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
|
||||||
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
|
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
|
||||||
}
|
}
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
|
||||||
@ -118,24 +113,30 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.push({
|
data.push({
|
||||||
|
itemStyle: {
|
||||||
|
color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||||
|
},
|
||||||
value: pool.share,
|
value: pool.share,
|
||||||
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
|
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
|
||||||
label: {
|
label: {
|
||||||
color: '#FFFFFF',
|
color: '#b1b1b1',
|
||||||
overflow: 'break',
|
overflow: 'break',
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
backgroundColor: '#282d47',
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#FFFFFF',
|
color: '#b1b1b1',
|
||||||
},
|
},
|
||||||
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
if (this.poolsWindowPreference === '24h') {
|
if (this.poolsWindowPreference === '24h') {
|
||||||
return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
||||||
`<br>` + pool.blockCount.toString() + ` blocks`;
|
`<br>` + pool.blockCount.toString() + ` blocks`;
|
||||||
} else {
|
} else {
|
||||||
return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
pool.blockCount.toString() + ` blocks`;
|
pool.blockCount.toString() + ` blocks`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,7 +154,13 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
network = network.charAt(0).toUpperCase() + network.slice(1);
|
network = network.charAt(0).toUpperCase() + network.slice(1);
|
||||||
|
|
||||||
|
let radius: any[] = ['20%', '70%'];
|
||||||
|
if (this.isMobile() || this.widget) {
|
||||||
|
radius = ['20%', '60%'];
|
||||||
|
}
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
|
color: chartColors,
|
||||||
title: {
|
title: {
|
||||||
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
|
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
@ -166,17 +173,21 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item'
|
trigger: 'item',
|
||||||
|
textStyle: {
|
||||||
|
align: 'left',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
top: this.widget ? '0%' : (this.isMobile() ? '5%' : '10%'),
|
top: this.widget ? 0 : 35,
|
||||||
bottom: this.widget ? '0%' : (this.isMobile() ? '0%' : '5%'),
|
|
||||||
name: 'Mining pool',
|
name: 'Mining pool',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: this.widget ? ['20%', '60%'] : (this.isMobile() ? ['10%', '50%'] : ['20%', '70%']),
|
radius: radius,
|
||||||
data: this.generatePoolsChartSerieData(miningStats),
|
data: this.generatePoolsChartSerieData(miningStats),
|
||||||
labelLine: {
|
labelLine: {
|
||||||
|
length: this.isMobile() ? 10 : 15,
|
||||||
|
length2: this.isMobile() ? 0 : 15,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
@ -202,7 +213,6 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
color: chartColors
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,9 +172,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right nowrap amount">
|
<td class="text-right nowrap amount">
|
||||||
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
|
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
|
||||||
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset]">
|
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset] else assetNotFound">
|
||||||
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
|
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #assetNotFound>
|
||||||
|
{{ vout.value }} <a [routerLink]="['/assets/asset/' | relativeUrl, vout.asset]">{{ vout.asset | slice : 0 : 7 }}</a>
|
||||||
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #defaultOutput>
|
<ng-template #defaultOutput>
|
||||||
<app-amount [satoshis]="vout.value"></app-amount>
|
<app-amount [satoshis]="vout.value"></app-amount>
|
||||||
@ -254,7 +257,7 @@
|
|||||||
|
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
|
||||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||||
<ng-template #defaultAmount>
|
<ng-template #defaultAmount>
|
||||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -95,6 +95,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
haveBlindedOutputValues(tx: Transaction): boolean {
|
||||||
|
return tx.vout.some((v: any) => v.value === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
getTotalTxOutput(tx: Transaction) {
|
getTotalTxOutput(tx: Transaction) {
|
||||||
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
return tx.vout.map((v: any) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||||
}
|
}
|
||||||
|
@ -16,58 +16,79 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #expanded>
|
<ng-template #expanded>
|
||||||
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
<ng-container *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
||||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
<div class="col card-wrapper">
|
||||||
<div class="card">
|
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||||
<div class="card-body">
|
<div class="card">
|
||||||
<app-fees-box class="d-block"></app-fees-box>
|
<div class="card-body">
|
||||||
|
<app-fees-box class="d-block"></app-fees-box>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col">
|
||||||
<div class="col" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
|
<app-difficulty></app-difficulty>
|
||||||
<app-difficulty></app-difficulty>
|
</div>
|
||||||
</div>
|
</ng-container>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card graph-card">
|
<div class="card graph-card">
|
||||||
<div class="card-body pl-0">
|
<div class="card-body pl-0">
|
||||||
<div style="padding-left: 1.25rem;">
|
<div style="padding-left: 1.25rem;">
|
||||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
<ng-template [ngIf]="(network$ | async) !== 'liquid'" [ngIfElse]="liquidPegs">
|
||||||
<div class="mempool-graph">
|
<ng-container *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||||
<app-mempool-graph
|
<div class="mempool-graph">
|
||||||
[template]="'widget'"
|
<app-mempool-graph
|
||||||
[limitFee]="150"
|
[template]="'widget'"
|
||||||
[limitFilterFee]="1"
|
[limitFee]="150"
|
||||||
[data]="mempoolStats.value?.mempool"
|
[limitFilterFee]="1"
|
||||||
[windowPreferenceOverride]="'2h'"
|
[data]="mempoolStats.value?.mempool"
|
||||||
></app-mempool-graph>
|
[windowPreferenceOverride]="'2h'"
|
||||||
</div>
|
></app-mempool-graph>
|
||||||
</ng-container>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #liquidPegs>
|
||||||
|
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card graph-card">
|
<div class="card graph-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
<ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? mempoolTable : txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
|
<div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph">
|
||||||
<app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph>
|
<table class="table table-borderless table-striped" *ngIf="(featuredAssets$ | async) as featuredAssets else loadingAssetsTable">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let group of featuredAssets">
|
||||||
|
<td class="asset-icon">
|
||||||
|
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
|
||||||
|
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="asset-title">
|
||||||
|
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="circulating-amount"><app-asset-circulation [assetId]="group.asset"></app-asset-circulation></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #mempoolGraph>
|
<ng-template #mempoolGraph>
|
||||||
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||||
@ -158,6 +179,27 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #loadingAssetsTable>
|
||||||
|
<table class="table table-borderless table-striped asset-table">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let i of [1,2,3,4]">
|
||||||
|
<td class="asset-icon">
|
||||||
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
|
</td>
|
||||||
|
<td class="asset-title">
|
||||||
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
|
</td>
|
||||||
|
<td class="asset-title d-none d-md-table-cell">
|
||||||
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
|
</td>
|
||||||
|
<td class="asset-title">
|
||||||
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #loadingTransactions>
|
<ng-template #loadingTransactions>
|
||||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -283,3 +283,25 @@
|
|||||||
margin-right: -2px;
|
margin-right: -2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assetIcon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-title {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-icon {
|
||||||
|
width: 65px;
|
||||||
|
height: 65px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circulating-amount {
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { combineLatest, merge, Observable, of, timer } from 'rxjs';
|
import { combineLatest, merge, Observable, of } from 'rxjs';
|
||||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||||
@ -34,6 +34,7 @@ interface MempoolStatsData {
|
|||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit {
|
export class DashboardComponent implements OnInit {
|
||||||
collapseLevel: string;
|
collapseLevel: string;
|
||||||
|
featuredAssets$: Observable<any>;
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||||
@ -124,6 +125,19 @@ export class DashboardComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.featuredAssets$ = this.apiService.listFeaturedAssets$()
|
||||||
|
.pipe(
|
||||||
|
map((featured) => {
|
||||||
|
const newArray = [];
|
||||||
|
for (const feature of featured) {
|
||||||
|
if (feature.ticker !== 'L-BTC' && feature.asset) {
|
||||||
|
newArray.push(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newArray.slice(0, 4);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
this.blocks$ = this.stateService.blocks$
|
this.blocks$ = this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(([block]) => {
|
tap(([block]) => {
|
||||||
|
@ -101,6 +101,7 @@ export interface PoolStat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockExtension {
|
export interface BlockExtension {
|
||||||
|
totalFees?: number;
|
||||||
medianFee?: number;
|
medianFee?: number;
|
||||||
feeRange?: number[];
|
feeRange?: number[];
|
||||||
reward?: number;
|
reward?: number;
|
||||||
|
@ -156,4 +156,18 @@ export class ApiService {
|
|||||||
(interval !== undefined ? `/${interval}` : '')
|
(interval !== undefined ? `/${interval}` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
|
||||||
|
return this.httpClient.get<any[]>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
|
||||||
|
(interval !== undefined ? `/${interval}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistoricalPoolsHashrate$(interval: string | undefined): Observable<any> {
|
||||||
|
return this.httpClient.get<any[]>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate/pools` +
|
||||||
|
(interval !== undefined ? `/${interval}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ export class MiningService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const availableTimespanDay = (
|
const availableTimespanDay = (
|
||||||
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp / 1000)
|
(new Date().getTime() / 1000) - (stats.oldestIndexedBlockTimestamp)
|
||||||
) / 3600 / 24;
|
) / 3600 / 24;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
22
frontend/src/app/shared/pipes/amount-shortener.pipe.ts
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
BIN
frontend/src/resources/profile/nix-bitcoin.png
Normal file
After Width: | Height: | Size: 18 KiB |
@ -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 |
@ -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 |
@ -21,6 +21,18 @@ do for url in / \
|
|||||||
'/api/v1/mining/pools/2y' \
|
'/api/v1/mining/pools/2y' \
|
||||||
'/api/v1/mining/pools/3y' \
|
'/api/v1/mining/pools/3y' \
|
||||||
'/api/v1/mining/pools/all' \
|
'/api/v1/mining/pools/all' \
|
||||||
|
'/api/v1/mining/hashrate/3m' \
|
||||||
|
'/api/v1/mining/hashrate/6m' \
|
||||||
|
'/api/v1/mining/hashrate/1y' \
|
||||||
|
'/api/v1/mining/hashrate/2y' \
|
||||||
|
'/api/v1/mining/hashrate/3y' \
|
||||||
|
'/api/v1/mining/hashrate/all' \
|
||||||
|
'/api/v1/mining/hashrate/pools/3m' \
|
||||||
|
'/api/v1/mining/hashrate/pools/6m' \
|
||||||
|
'/api/v1/mining/hashrate/pools/1y' \
|
||||||
|
'/api/v1/mining/hashrate/pools/2y' \
|
||||||
|
'/api/v1/mining/hashrate/pools/3y' \
|
||||||
|
'/api/v1/mining/hashrate/pools/all' \
|
||||||
|
|
||||||
do
|
do
|
||||||
curl -s "https://${hostname}${url}" >/dev/null
|
curl -s "https://${hostname}${url}" >/dev/null
|
||||||
|