Merge branch 'master' into mononaut/seo-ssr

This commit is contained in:
wiz 2023-03-08 15:29:18 +09:00 committed by GitHub
commit a874cdfb56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 4211 additions and 3111 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
backend/src/api/database-migration.ts @wiz @softsimon

View File

@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
import mempool from '../mempool'; import mempool from '../mempool';
import feeApi from '../fee-api'; import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks'; import mempoolBlocks from '../mempool-blocks';
import bitcoinApi from './bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
import { Common } from '../common'; import { Common } from '../common';
import backendInfo from '../backend-info'; import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils'; import transactionUtils from '../transaction-utils';
@ -220,18 +220,17 @@ class BitcoinRoutes {
let cpfpInfo; let cpfpInfo;
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
}
if (cpfpInfo) {
res.json(cpfpInfo);
return;
} else { } else {
res.json({ res.json({
ancestors: [] ancestors: []
}); });
return; return;
} }
if (cpfpInfo) {
res.json(cpfpInfo);
return;
}
} }
res.status(404).send(`Transaction has no CPFP info available.`);
} }
private getBackendInfo(req: Request, res: Response) { private getBackendInfo(req: Request, res: Response) {
@ -469,7 +468,7 @@ class BitcoinRoutes {
returnBlocks.push(localBlock); returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash; nextHash = localBlock.previousblockhash;
} else { } else {
const block = await bitcoinApi.$getBlock(nextHash); const block = await bitcoinCoreApi.$getBlock(nextHash);
returnBlocks.push(block); returnBlocks.push(block);
nextHash = block.previousblockhash; nextHash = block.previousblockhash;
} }
@ -652,7 +651,7 @@ class BitcoinRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
res.status(404).send('not found'); res.status(204).send();
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);

View File

@ -1,5 +1,5 @@
import config from '../config'; import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } from '../mempool.interfaces';
@ -484,7 +484,7 @@ class Blocks {
loadingIndicators.setProgress('block-indexing', progress, false); loadingIndicators.setProgress('block-indexing', progress, false);
} }
const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
@ -532,13 +532,13 @@ class Blocks {
if (blockchainInfo.blocks === blockchainInfo.headers) { if (blockchainInfo.blocks === blockchainInfo.headers) {
const heightDiff = blockHeightTip % 2016; const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff); const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
this.lastDifficultyAdjustmentTime = block.timestamp; this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty; this.currentDifficulty = block.difficulty;
if (blockHeightTip >= 2016) { if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016); const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash); const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100; this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`); logger.debug(`Initial difficulty adjustment data set.`);
} }
@ -662,7 +662,7 @@ class Blocks {
} }
const blockHash = await bitcoinApi.$getBlockHash(height); const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
@ -685,11 +685,11 @@ class Blocks {
// Not Bitcoin network, return the block as it from the bitcoin backend // Not Bitcoin network, return the block as it from the bitcoin backend
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return await bitcoinApi.$getBlock(hash); return await bitcoinCoreApi.$getBlock(hash);
} }
// Bitcoin network, add our custom data on top // Bitcoin network, add our custom data on top
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
return await this.$indexBlock(block.height); return await this.$indexBlock(block.height);
} }

View File

@ -175,6 +175,7 @@ export class Common {
case '1y': return '1 YEAR'; case '1y': return '1 YEAR';
case '2y': return '2 YEAR'; case '2y': return '2 YEAR';
case '3y': return '3 YEAR'; case '3y': return '3 YEAR';
case '4y': return '4 YEAR';
default: return null; default: return null;
} }
} }

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 57; private static currentVersion = 58;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -505,6 +505,11 @@ class DatabaseMigration {
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`); await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
await this.updateToSchemaVersion(57); await this.updateToSchemaVersion(57);
} }
if (databaseSchemaVersion < 58) {
// We only run some migration queries for this version
await this.updateToSchemaVersion(58);
}
} }
/** /**
@ -632,6 +637,11 @@ class DatabaseMigration {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`); queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`);
} }
if (version < 58) {
queries.push(`DELETE FROM state WHERE name = 'last_hashrates_indexing'`);
queries.push(`DELETE FROM state WHERE name = 'last_weekly_hashrates_indexing'`);
}
return queries; return queries;
} }
@ -1023,10 +1033,11 @@ class DatabaseMigration {
await this.$executeQuery(`TRUNCATE blocks`); await this.$executeQuery(`TRUNCATE blocks`);
await this.$executeQuery(`TRUNCATE hashrates`); await this.$executeQuery(`TRUNCATE hashrates`);
await this.$executeQuery(`TRUNCATE difficulty_adjustments`);
await this.$executeQuery('DELETE FROM `pools`'); await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
} }
private async $convertCompactCpfpTables(): Promise<void> { private async $convertCompactCpfpTables(): Promise<void> {
try { try {

View File

@ -62,7 +62,7 @@ class DiskCache {
} }
wipeCache() { wipeCache() {
logger.notice(`Wipping nodejs backend cache/cache*.json files`); logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try { try {
fs.unlinkSync(DiskCache.FILE_NAME); fs.unlinkSync(DiskCache.FILE_NAME);
} catch (e: any) { } catch (e: any) {

View File

@ -97,14 +97,14 @@ class MempoolBlocks {
blockSize += tx.size; blockSize += tx.size;
transactions.push(tx); transactions.push(tx);
} else { } else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
blockWeight = tx.weight; blockWeight = tx.weight;
blockSize = tx.size; blockSize = tx.size;
transactions = [tx]; transactions = [tx];
} }
}); });
if (transactions.length) { if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
} }
return mempoolBlocks; return mempoolBlocks;
@ -281,7 +281,7 @@ class MempoolBlocks {
const mempoolBlocks = blocks.map((transactions, blockIndex) => { const mempoolBlocks = blocks.map((transactions, blockIndex) => {
return this.dataToMempoolBlocks(transactions.map(tx => { return this.dataToMempoolBlocks(transactions.map(tx => {
return mempool[tx.txid] || null; return mempool[tx.txid] || null;
}).filter(tx => !!tx), undefined, undefined, blockIndex); }).filter(tx => !!tx), blockIndex);
}); });
if (saveResults) { if (saveResults) {
@ -293,18 +293,17 @@ class MempoolBlocks {
return mempoolBlocks; return mempoolBlocks;
} }
private dataToMempoolBlocks(transactions: TransactionExtended[], private dataToMempoolBlocks(transactions: TransactionExtended[], blocksIndex: number): MempoolBlockWithTransactions {
blockSize: number | undefined, blockWeight: number | undefined, blocksIndex: number): MempoolBlockWithTransactions { let totalSize = 0;
let totalSize = blockSize || 0; let totalWeight = 0;
let totalWeight = blockWeight || 0; const fitTransactions: TransactionExtended[] = [];
if (blockSize === undefined && blockWeight === undefined) { transactions.forEach(tx => {
totalSize = 0; totalSize += tx.size;
totalWeight = 0; totalWeight += tx.weight;
transactions.forEach(tx => { if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
totalSize += tx.size; fitTransactions.push(tx);
totalWeight += tx.weight; }
}); });
}
let rangeLength = 4; let rangeLength = 4;
if (blocksIndex === 0) { if (blocksIndex === 0) {
rangeLength = 8; rangeLength = 8;
@ -322,7 +321,7 @@ class MempoolBlocks {
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: Common.getFeesInRange(transactions, rangeLength), feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid), transactionIds: transactions.map((tx) => tx.txid),
transactions: transactions.map((tx) => Common.stripTransaction(tx)), transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
}; };
} }
} }

View File

@ -263,7 +263,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(404).send(`This block has not been audited.`); res.status(204).send(`This block has not been audited.`);
return; return;
} }

View File

@ -11,14 +11,13 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
import config from '../../config'; import config from '../../config';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import bitcoinApiFactory from '../bitcoin/bitcoin-api-factory'; import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface'; import { IEsploraApi } from '../bitcoin/esplora-api.interface';
class Mining { class Mining {
blocksPriceIndexingRunning = false; private blocksPriceIndexingRunning = false;
public lastHashrateIndexingDate: number | null = null;
constructor() { public lastWeeklyHashrateIndexingDate: number | null = null;
}
/** /**
* Get historical block predictions match rate * Get historical block predictions match rate
@ -118,7 +117,7 @@ class Mining {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return poolsStatistics; return poolsStatistics;
@ -146,7 +145,7 @@ class Mining {
try { try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
} }
return { return {
@ -178,20 +177,21 @@ class Mining {
*/ */
public async $generatePoolHashrateHistory(): Promise<void> { public async $generatePoolHashrateHistory(): Promise<void> {
const now = new Date(); const now = new Date();
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
// Run only if: // Run only if:
// * lastestRunDate is set to 0 (node backend restart, reorg) // * this.lastWeeklyHashrateIndexingDate is set to null (node backend restart, reorg)
// * we started a new week (around Monday midnight) // * we started a new week (around Monday midnight)
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate(); const runIndexing = this.lastWeeklyHashrateIndexingDate === null ||
now.getUTCDay() === 1 && this.lastWeeklyHashrateIndexingDate !== now.getUTCDate();
if (!runIndexing) { if (!runIndexing) {
logger.debug(`Pool hashrate history indexing is up to date, nothing to do`, logger.tags.mining);
return; return;
} }
try { try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp; const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock: IEsploraApi.Block = await bitcoinApiFactory.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000; const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps(); const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
@ -208,7 +208,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`); logger.debug(`Indexing weekly mining pool hashrate`, logger.tags.mining);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0); loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@ -245,7 +245,7 @@ class Mining {
}); });
} }
newlyIndexed += hashrates.length; newlyIndexed += hashrates.length / Math.max(1, pools.length);
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0; hashrates.length = 0;
} }
@ -256,7 +256,7 @@ class Mining {
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false); loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@ -266,16 +266,16 @@ class Mining {
++indexedThisRun; ++indexedThisRun;
++totalIndexed; ++totalIndexed;
} }
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate()); this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.info(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} else { } else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
} }
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@ -285,16 +285,16 @@ class Mining {
*/ */
public async $generateNetworkHashrateHistory(): Promise<void> { public async $generateNetworkHashrateHistory(): Promise<void> {
// We only run this once a day around midnight // We only run this once a day around midnight
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing'); const today = new Date().getUTCDate();
const now = new Date().getUTCDate(); if (today === this.lastHashrateIndexingDate) {
if (now === latestRunDate) { logger.debug(`Network hashrate history indexing is up to date, nothing to do`, logger.tags.mining);
return; return;
} }
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp; const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try { try {
const genesisBlock: IEsploraApi.Block = await bitcoinApiFactory.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000; const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date()); const lastMidnight = this.getDateMidnight(new Date());
@ -308,7 +308,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`); logger.debug(`Indexing daily network hashrate`, logger.tags.mining);
loadingIndicators.setProgress('daily-hashrate-indexing', 0); loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@ -346,7 +346,7 @@ class Mining {
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`); logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress); loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@ -371,16 +371,16 @@ class Mining {
newlyIndexed += hashrates.length; newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate()); this.lastHashrateIndexingDate = new Date().getUTCDate();
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.info(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} else { } else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} }
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Daily network hashrate indexing failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@ -396,7 +396,7 @@ class Mining {
} }
const blocks: any = await BlocksRepository.$getBlocksDifficulty(); const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinApiFactory.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty; let currentDifficulty = genesisBlock.difficulty;
let totalIndexed = 0; let totalIndexed = 0;
@ -446,13 +446,13 @@ class Mining {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100); const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`); logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
} }
} }
if (totalIndexed > 0) { if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} else { } else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} }
@ -499,7 +499,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0; blocksPrices.length = 0;
} }
@ -511,7 +511,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr); logger.debug(logStr, logger.tags.mining);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
} }
} catch (e) { } catch (e) {
@ -568,6 +568,7 @@ class Mining {
private getTimeRange(interval: string | null, scale = 1): number { private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) { switch (interval) {
case '4y': return 43200 * scale; // 12h
case '3y': return 43200 * scale; // 12h case '3y': return 43200 * scale; // 12h
case '2y': return 28800 * scale; // 8h case '2y': return 28800 * scale; // 8h
case '1y': return 28800 * scale; // 8h case '1y': return 28800 * scale; // 8h

View File

@ -39,6 +39,10 @@ class PoolsParser {
* @param pools * @param pools
*/ */
public async migratePoolsJson(): Promise<void> { public async migratePoolsJson(): Promise<void> {
// We also need to wipe the backend cache to make sure we don't serve blocks with
// the wrong mining pool (usually happen with unknown blocks)
diskCache.wipeCache();
await this.$insertUnknownPool(); await this.$insertUnknownPool();
for (const pool of this.miningPools) { for (const pool of this.miningPools) {
@ -142,10 +146,6 @@ class PoolsParser {
WHERE pool_id = ?`, WHERE pool_id = ?`,
[pool.id] [pool.id]
); );
// We also need to wipe the backend cache to make sure we don't serve blocks with
// the wrong mining pool (usually happen with unknown blocks)
diskCache.wipeCache();
} }
private async $deleteUnknownBlocks(): Promise<void> { private async $deleteUnknownBlocks(): Promise<void> {
@ -156,10 +156,6 @@ class PoolsParser {
WHERE pool_id = ? AND height >= 130635`, WHERE pool_id = ? AND height >= 130635`,
[unknownPool[0].id] [unknownPool[0].id]
); );
// We also need to wipe the backend cache to make sure we don't serve blocks with
// the wrong mining pool (usually happen with unknown blocks)
diskCache.wipeCache();
} }
} }

View File

@ -375,6 +375,17 @@ class StatisticsApi {
} }
} }
public async $list4Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(43200, '4 YEAR'); // 12h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {
logger.err('$list4Y() error' + (e instanceof Error ? e.message : e));
return [];
}
}
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] { private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => { return statistic.map((s) => {
return { return {

View File

@ -14,10 +14,11 @@ class StatisticsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
; ;
} }
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) { private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@ -54,6 +55,9 @@ class StatisticsRoutes {
case '3y': case '3y':
result = await statisticsApi.$list3Y(); result = await statisticsApi.$list3Y();
break; break;
case '4y':
result = await statisticsApi.$list4Y();
break;
default: default:
result = await statisticsApi.$list2H(); result = await statisticsApi.$list2H();
} }

View File

@ -1,8 +1,8 @@
import logger from '../logger'; import logger from '../logger';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta, BlockExtended, TransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, IConversionRates OptimizedStatistic, ILoadingIndicators
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
@ -20,6 +20,7 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit'; import Audit from './audit';
import { deepClone } from '../utils/clone'; import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -193,7 +194,7 @@ class WebsocketHandler {
}); });
} }
handleNewConversionRates(conversionRates: IConversionRates) { handleNewConversionRates(conversionRates: ApiPrice) {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
@ -214,7 +215,7 @@ class WebsocketHandler {
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(), 'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks, 'blocks': _blocks,
'conversions': priceUpdater.latestPrices, 'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(), 'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(), 'backendInfo': backendInfo.getBackendInfo(),

View File

@ -38,6 +38,8 @@ import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips'; import chainTips from './api/chain-tips';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -45,6 +47,11 @@ class Server {
private app: Application; private app: Application;
private currentBackendRetryInterval = 5; private currentBackendRetryInterval = 5;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
constructor() { constructor() {
this.app = express(); this.app = express();
@ -87,9 +94,6 @@ class Server {
await databaseMigration.$blocksReindexingTruncate(); await databaseMigration.$blocksReindexingTruncate();
} }
await databaseMigration.$initializeOrMigrateDatabase(); await databaseMigration.$initializeOrMigrateDatabase();
if (Common.indexingEnabled()) {
await indexer.$resetHashratesIndexingState();
}
} catch (e) { } catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error'); throw new Error(e instanceof Error ? e.message : 'Error');
} }
@ -113,6 +117,7 @@ class Server {
this.setUpWebsocketHandling(); this.setUpWebsocketHandling();
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$(); await syncAssets.syncAssets$();
if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.ENABLED) {
diskCache.loadMempoolCache(); diskCache.loadMempoolCache();
@ -139,6 +144,8 @@ class Server {
this.runMainUpdateLoop(); this.runMainUpdateLoop();
} }
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {
bisq.startBisqService(); bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
@ -171,7 +178,6 @@ class Server {
logger.debug(msg); logger.debug(msg);
} }
} }
await poolsUpdater.updatePoolsJson();
await blocks.$updateBlocks(); await blocks.$updateBlocks();
await memPool.$updateMempool(); await memPool.$updateMempool();
indexer.$run(); indexer.$run();
@ -258,6 +264,26 @@ class Server {
channelsRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app);
} }
} }
healthCheck(): void {
const now = Date.now();
const stats = v8.getHeapStatistics();
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
const warnThreshold = 0.8 * stats.heap_size_limit;
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true;
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
}
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
this.warnedHeapCritical = false;
this.maxHeapSize = 0;
this.lastHeapLogTime = now;
}
}
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@ -3,7 +3,6 @@ import blocks from './api/blocks';
import mempool from './api/mempool'; import mempool from './api/mempool';
import mining from './api/mining/mining'; import mining from './api/mining/mining';
import logger from './logger'; import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository';
import bitcoinClient from './api/bitcoin/bitcoin-client'; import bitcoinClient from './api/bitcoin/bitcoin-client';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import PricesRepository from './repositories/PricesRepository'; import PricesRepository from './repositories/PricesRepository';
@ -77,13 +76,13 @@ class Indexer {
this.tasksRunning.push(task); this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) { if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`); logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => { setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
}, 10000); }, 10000);
} else { } else {
logger.debug(`Blocks prices indexer will run now`); logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices(); await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
} }
@ -113,7 +112,7 @@ class Indexer {
this.runIndexer = false; this.runIndexer = false;
this.indexerRunning = true; this.indexerRunning = true;
logger.info(`Running mining indexer`); logger.debug(`Running mining indexer`);
await this.checkAvailableCoreIndexes(); await this.checkAvailableCoreIndexes();
@ -123,7 +122,7 @@ class Indexer {
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`); logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining);
setTimeout(() => this.reindex(), 10000); setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false; this.indexerRunning = false;
return; return;
@ -131,7 +130,6 @@ class Indexer {
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
await this.$resetHashratesIndexingState(); // TODO - Remove this as it's not efficient
await mining.$generateNetworkHashrateHistory(); await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory(); await mining.$generatePoolHashrateHistory();
await blocks.$generateBlocksSummariesDatabase(); await blocks.$generateBlocksSummariesDatabase();
@ -150,16 +148,6 @@ class Indexer {
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`); logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
setTimeout(() => this.reindex(), runEvery); setTimeout(() => this.reindex(), runEvery);
} }
async $resetHashratesIndexingState(): Promise<void> {
try {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
} }
export default new Indexer(); export default new Indexer();

View File

@ -293,7 +293,6 @@ interface RequiredParams {
} }
export interface ILoadingIndicators { [name: string]: number; } export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo { export interface IBackendInfo {
hostname: string; hostname: string;

View File

@ -748,6 +748,7 @@ class BlocksRepository {
SELECT height SELECT height
FROM compact_cpfp_clusters FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ? WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC; ORDER BY height DESC;
`, [currentBlockHeight, minHeight]); `, [currentBlockHeight, minHeight]);

View File

@ -1,5 +1,4 @@
import { Common } from '../api/common'; import { Common } from '../api/common';
import config from '../config';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { IndexedDifficultyAdjustment } from '../mempool.interfaces'; import { IndexedDifficultyAdjustment } from '../mempool.interfaces';
@ -21,9 +20,9 @@ class DifficultyAdjustmentsRepository {
await DB.query(query, params); await DB.query(query, params);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`); logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`, logger.tags.mining);
} else { } else {
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
@ -55,7 +54,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -84,7 +83,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -94,27 +93,27 @@ class DifficultyAdjustmentsRepository {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`); const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height); return rows.map(block => block.height);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
public async $deleteAdjustementsFromHeight(height: number): Promise<void> { public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
try { try {
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`); logger.info(`Delete newer difficulty adjustments from height ${height} from the database`, logger.tags.mining);
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]); await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }
public async $deleteLastAdjustment(): Promise<void> { public async $deleteLastAdjustment(): Promise<void> {
try { try {
logger.info(`Delete last difficulty adjustment from the database`); logger.info(`Delete last difficulty adjustment from the database`, logger.tags.mining);
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`); await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e; throw e;
} }
} }

View File

@ -1,5 +1,6 @@
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import { Common } from '../api/common'; import { Common } from '../api/common';
import mining from '../api/mining/mining';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
@ -24,7 +25,7 @@ class HashratesRepository {
try { try {
await DB.query(query); await DB.query(query);
} catch (e: any) { } catch (e: any) {
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -50,7 +51,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -77,7 +78,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -92,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp); return rows.map(row => row.timestamp);
} catch (e) { } catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -127,7 +128,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -157,7 +158,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [pool.id]); const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0]; boundaries = rows[0];
} catch (e) { } catch (e) {
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
// Get hashrates entries between boundaries // Get hashrates entries between boundaries
@ -172,21 +173,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]); const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e;
}
}
/**
* Set latest run timestamp
*/
public async $setLatestRun(key: string, val: number) {
const query = `UPDATE state SET number = ? WHERE name = ?`;
try {
await DB.query(query, [val, key]);
} catch (e) {
logger.err(`Cannot set last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@ -205,7 +192,7 @@ class HashratesRepository {
} }
return rows[0]['number']; return rows[0]['number'];
} catch (e) { } catch (e) {
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e; throw e;
} }
} }
@ -214,7 +201,7 @@ class HashratesRepository {
* Delete most recent data points for re-indexing * Delete most recent data points for re-indexing
*/ */
public async $deleteLastEntries() { public async $deleteLastEntries() {
logger.info(`Delete latest hashrates data points from the database`); logger.info(`Delete latest hashrates data points from the database`, logger.tags.mining);
try { try {
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`); const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
@ -222,10 +209,10 @@ class HashratesRepository {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]); await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]);
} }
// Re-run the hashrate indexing to fill up missing data // Re-run the hashrate indexing to fill up missing data
await this.$setLatestRun('last_hashrates_indexing', 0); mining.lastHashrateIndexingDate = null;
await this.$setLatestRun('last_weekly_hashrates_indexing', 0); mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
@ -238,10 +225,10 @@ class HashratesRepository {
try { try {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]); await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);
// Re-run the hashrate indexing to fill up missing data // Re-run the hashrate indexing to fill up missing data
await this.$setLatestRun('last_hashrates_indexing', 0); mining.lastHashrateIndexingDate = null;
await this.$setLatestRun('last_weekly_hashrates_indexing', 0); mining.lastWeeklyHashrateIndexingDate = null;
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
} }

View File

@ -1,6 +1,5 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
export interface ApiPrice { export interface ApiPrice {
@ -13,6 +12,16 @@ export interface ApiPrice {
AUD: number, AUD: number,
JPY: number, JPY: number,
} }
const ApiPriceFields = `
UNIX_TIMESTAMP(time) as time,
USD,
EUR,
GBP,
CAD,
CHF,
AUD,
JPY
`;
export interface ExchangeRates { export interface ExchangeRates {
USDEUR: number, USDEUR: number,
@ -39,7 +48,7 @@ export const MAX_PRICES = {
}; };
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: IConversionRates): Promise<void> { public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
if (prices.USD === -1) { if (prices.USD === -1) {
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues // Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine // As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
@ -60,77 +69,115 @@ class PricesRepository {
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`, VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY] [time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
); );
} catch (e: any) { } catch (e) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
public async $getOldestPriceTime(): Promise<number> { public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`); const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time
LIMIT 1
`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getLatestPriceId(): Promise<number | null> { public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`); const [oldestRow] = await DB.query(`
return oldestRow[0] ? oldestRow[0].id : null; SELECT id
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
return times;
}
public async $getLatestConversionRates(): Promise<any> {
const [rates]: any[] = await DB.query(`
SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
LIMIT 1` LIMIT 1`
); );
if (!rates || rates.length === 0) { return oldestRow[0] ? oldestRow[0].id : null;
}
public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time DESC
LIMIT 1`
);
return oldestRow[0] ? oldestRow[0].time : 0;
}
public async $getPricesTimes(): Promise<number[]> {
const [times] = await DB.query(`
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
WHERE USD != -1
ORDER BY time
`);
if (!Array.isArray(times)) {
return [];
}
return times.map(time => time.time);
}
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
const [times] = await DB.query(`
SELECT
UNIX_TIMESTAMP(time) AS time,
id,
USD
FROM prices
ORDER BY time
`);
return times as {time: number, id: number, USD: number}[];
}
public async $getLatestConversionRates(): Promise<ApiPrice> {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
LIMIT 1`
);
if (!Array.isArray(rates) || rates.length === 0) {
return priceUpdater.getEmptyPricesObj(); return priceUpdater.getEmptyPricesObj();
} }
return rates[0]; return rates[0] as ApiPrice;
} }
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> { public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
WHERE UNIX_TIMESTAMP(time) < ? WHERE UNIX_TIMESTAMP(time) < ?
ORDER BY time DESC ORDER BY time DESC
LIMIT 1`, LIMIT 1`,
[timestamp] [timestamp]
); );
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get single historical price from the database`); throw Error(`Cannot get single historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice = await this.$getLatestConversionRates(); let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {
@ -141,28 +188,35 @@ class PricesRepository {
public async $getHistoricalPrices(): Promise<Conversion | null> { public async $getHistoricalPrices(): Promise<Conversion | null> {
try { try {
const [rates]: any[] = await DB.query(` const [rates] = await DB.query(`
SELECT *, UNIX_TIMESTAMP(time) AS time SELECT ${ApiPriceFields}
FROM prices FROM prices
ORDER BY time DESC ORDER BY time DESC
`); `);
if (!rates) { if (!Array.isArray(rates)) {
throw Error(`Cannot get average historical price from the database`); throw Error(`Cannot get average historical price from the database`);
} }
// Compute fiat exchange rates // Compute fiat exchange rates
const latestPrice: ApiPrice = rates[0]; let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = { const exchangeRates: ExchangeRates = {
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100, USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100, USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100, USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100, USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100, USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100, USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
}; };
return { return {
prices: rates, prices: rates as ApiPrice[],
exchangeRates: exchangeRates exchangeRates: exchangeRates
}; };
} catch (e) { } catch (e) {

View File

@ -411,7 +411,7 @@ class LightningStatsImporter {
} }
if (totalProcessed > 0) { if (totalProcessed > 0) {
logger.notice(`Lightning network stats historical import completed`, logger.tags.ln); logger.info(`Lightning network stats historical import completed`, logger.tags.ln);
} }
} catch (e) { } catch (e) {
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);

View File

@ -12,7 +12,7 @@ import * as https from 'https';
*/ */
class PoolsUpdater { class PoolsUpdater {
lastRun: number = 0; lastRun: number = 0;
currentSha: string | undefined = undefined; currentSha: string | null = null;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
@ -33,7 +33,7 @@ class PoolsUpdater {
try { try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
if (githubSha === undefined) { if (githubSha === null) {
return; return;
} }
@ -42,12 +42,12 @@ class PoolsUpdater {
} }
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== undefined && this.currentSha === githubSha) { if (this.currentSha !== null && this.currentSha === githubSha) {
return; return;
} }
// See backend README for more details about the mining pools update process // See backend README for more details about the mining pools update process
if (this.currentSha !== undefined && // If we don't have any mining pool, download it at least once if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool !process.env.npm_config_update_pools // We're not manually updating mining pool
) { ) {
@ -57,7 +57,7 @@ class PoolsUpdater {
} }
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === undefined) { if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else { } else {
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
@ -82,7 +82,7 @@ class PoolsUpdater {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;'); await DB.query('ROLLBACK;');
} }
logger.notice('PoolsUpdater completed'); logger.info('PoolsUpdater completed');
} catch (e) { } catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
@ -108,20 +108,20 @@ class PoolsUpdater {
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools-v2.json sha from the db
*/ */
private async getShaFromDb(): Promise<string | undefined> { private async getShaFromDb(): Promise<string | null> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : undefined); return (rows.length > 0 ? rows[0].string : null);
} catch (e) { } catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return undefined; return null;
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from github * Fetch our latest pools-v2.json sha from github
*/ */
private async fetchPoolsSha(): Promise<string | undefined> { private async fetchPoolsSha(): Promise<string | null> {
const response = await this.query(this.treeUrl); const response = await this.query(this.treeUrl);
if (response !== undefined) { if (response !== undefined) {
@ -133,7 +133,7 @@ class PoolsUpdater {
} }
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return undefined; return null;
} }
/** /**

View File

@ -8,9 +8,6 @@ class BitfinexApi implements PriceFeed {
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC'; public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist'; public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['last_price']) { if (response && response['last_price']) {

View File

@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
} }
if (Object.keys(priceHistory).length > 0) { if (Object.keys(priceHistory).length > 0) {
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
} }
} }
} }

View File

@ -2,8 +2,7 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces'; import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository';
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api'; import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api'; import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api'; import CoinbaseApi from './price-feeds/coinbase-api';
@ -21,18 +20,18 @@ export interface PriceFeed {
} }
export interface PriceHistory { export interface PriceHistory {
[timestamp: number]: IConversionRates; [timestamp: number]: ApiPrice;
} }
class PriceUpdater { class PriceUpdater {
public historyInserted = false; public historyInserted = false;
lastRun = 0; private lastRun = 0;
lastHistoricalRun = 0; private lastHistoricalRun = 0;
running = false; private running = false;
feeds: PriceFeed[] = []; private feeds: PriceFeed[] = [];
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY']; private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
latestPrices: IConversionRates; private latestPrices: ApiPrice;
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined; private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() { constructor() {
this.latestPrices = this.getEmptyPricesObj(); this.latestPrices = this.getEmptyPricesObj();
@ -44,8 +43,13 @@ class PriceUpdater {
this.feeds.push(new GeminiApi()); this.feeds.push(new GeminiApi());
} }
public getEmptyPricesObj(): IConversionRates { public getLatestPrices(): ApiPrice {
return this.latestPrices;
}
public getEmptyPricesObj(): ApiPrice {
return { return {
time: 0,
USD: -1, USD: -1,
EUR: -1, EUR: -1,
GBP: -1, GBP: -1,
@ -56,7 +60,7 @@ class PriceUpdater {
}; };
} }
public setRatesChangedCallback(fn: (rates: IConversionRates) => void) { public setRatesChangedCallback(fn: (rates: ApiPrice) => void): void {
this.ratesChangedCallback = fn; this.ratesChangedCallback = fn;
} }
@ -156,6 +160,10 @@ class PriceUpdater {
} }
this.lastRun = new Date().getTime() / 1000; this.lastRun = new Date().getTime() / 1000;
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
}
} }
/** /**
@ -224,7 +232,7 @@ class PriceUpdater {
// Group them by timestamp and currency, for example // Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4]; // grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped: any = {}; const grouped = {};
for (const historicalEntry of historicalPrices) { for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) { for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) { if (existingPriceTimes.includes(parseInt(time, 10))) {
@ -249,7 +257,7 @@ class PriceUpdater {
// Average prices and insert everything into the db // Average prices and insert everything into the db
let totalInserted = 0; let totalInserted = 0;
for (const time in grouped) { for (const time in grouped) {
const prices: IConversionRates = this.getEmptyPricesObj(); const prices: ApiPrice = this.getEmptyPricesObj();
for (const currency in grouped[time]) { for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) { if (grouped[time][currency].length === 0) {
continue; continue;

View File

@ -0,0 +1,29 @@
const byteUnits = ['B', 'kB', 'MB', 'GB', 'TB'];
export function getBytesUnit(bytes: number): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return 'B';
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && bytes > 1024) {
unitIndex++;
bytes /= 1024;
}
return byteUnits[unitIndex];
}
export function formatBytes(bytes: number, toUnit: string, skipUnit = false): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return `${bytes}`;
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && (toUnit && byteUnits[unitIndex] !== toUnit || (!toUnit && bytes > 1024))) {
unitIndex++;
bytes /= 1024;
}
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
}

View File

@ -26,7 +26,7 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__, "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL__MAX_BLOCKS_BULK_QUERY__ "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'cypress' import { defineConfig } from 'cypress';
export default defineConfig({ export default defineConfig({
projectId: 'ry4br7', projectId: 'ry4br7',
@ -12,12 +12,18 @@ export default defineConfig({
}, },
chromeWebSecurity: false, chromeWebSecurity: false,
e2e: { e2e: {
// We've imported your old cypress plugins here. setupNodeEvents(on: any, config: any) {
// You may want to clean this up later by importing these. const fs = require('fs');
setupNodeEvents(on, config) { const CONFIG_FILE = 'mempool-frontend-config.json';
return require('./cypress/plugins/index.js')(on, config) if (fs.existsSync(CONFIG_FILE)) {
let contents = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
config.env.BASE_MODULE = contents.BASE_MODULE ? contents.BASE_MODULE : 'mempool';
} else {
config.env.BASE_MODULE = 'mempool';
}
return config;
}, },
baseUrl: 'http://localhost:4200', baseUrl: 'http://localhost:4200',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
}, },
}) });

View File

@ -1,5 +1,5 @@
describe('Bisq', () => { describe('Bisq', () => {
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
const basePath = ''; const basePath = '';
beforeEach(() => { beforeEach(() => {
@ -20,7 +20,7 @@ describe('Bisq', () => {
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
}); });
describe("transactions", () => { describe('transactions', () => {
it('loads the transactions screen', () => { it('loads the transactions screen', () => {
cy.visit(`${basePath}`); cy.visit(`${basePath}`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
@ -30,9 +30,9 @@ describe('Bisq', () => {
}); });
const filters = [ const filters = [
"Asset listing fee", "Blind vote", "Compensation request", 'Asset listing fee', 'Blind vote', 'Compensation request',
"Genesis", "Irregular", "Lockup", "Pay trade fee", "Proof of burn", 'Genesis', 'Irregular', 'Lockup', 'Pay trade fee', 'Proof of burn',
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal" 'Proposal', 'Reimbursement request', 'Transfer BSQ', 'Unlock', 'Vote reveal'
]; ];
filters.forEach((filter) => { filters.forEach((filter) => {
it.only(`filters the transaction screen by ${filter}`, () => { it.only(`filters the transaction screen by ${filter}`, () => {
@ -49,7 +49,7 @@ describe('Bisq', () => {
}); });
}); });
it("filters using multiple criteria", () => { it('filters using multiple criteria', () => {
const filters = ['Proposal', 'Lockup', 'Unlock']; const filters = ['Proposal', 'Lockup', 'Unlock'];
cy.visit(`${basePath}/transactions`); cy.visit(`${basePath}/transactions`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();

View File

@ -1,5 +1,5 @@
describe('Liquid', () => { describe('Liquid', () => {
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
const basePath = ''; const basePath = '';
beforeEach(() => { beforeEach(() => {

View File

@ -1,5 +1,5 @@
describe('Liquid Testnet', () => { describe('Liquid Testnet', () => {
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
const basePath = '/testnet'; const basePath = '/testnet';
beforeEach(() => { beforeEach(() => {

View File

@ -1,6 +1,6 @@
import { emitMempoolInfo, dropWebSocket } from "../../support/websocket"; import { emitMempoolInfo, dropWebSocket } from '../../support/websocket';
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
//Credit: https://github.com/bahmutov/cypress-examples/blob/6cedb17f83a3bb03ded13cf1d6a3f0656ca2cdf5/docs/recipes/overlapping-elements.md //Credit: https://github.com/bahmutov/cypress-examples/blob/6cedb17f83a3bb03ded13cf1d6a3f0656ca2cdf5/docs/recipes/overlapping-elements.md
@ -339,14 +339,14 @@ describe('Mainnet', () => {
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.changeNetwork("testnet"); cy.changeNetwork('testnet');
cy.changeNetwork("signet"); cy.changeNetwork('signet');
cy.changeNetwork("mainnet"); cy.changeNetwork('mainnet');
}); });
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit("/"); cy.visit('/');
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');

View File

@ -1,4 +1,4 @@
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
describe('Mainnet - Mining Features', () => { describe('Mainnet - Mining Features', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -1,6 +1,6 @@
import { emitMempoolInfo } from "../../support/websocket"; import { emitMempoolInfo } from '../../support/websocket';
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
describe('Signet', () => { describe('Signet', () => {
beforeEach(() => { beforeEach(() => {
@ -25,7 +25,7 @@ describe('Signet', () => {
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit("/signet"); cy.visit('/signet');
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
@ -35,7 +35,7 @@ describe('Signet', () => {
emitMempoolInfo({ emitMempoolInfo({
'params': { 'params': {
"network": "signet" 'network': 'signet'
} }
}); });

View File

@ -1,6 +1,6 @@
import { confirmAddress, emitMempoolInfo, sendWsMock, showNewTx, startTrackingAddress } from "../../support/websocket"; import { emitMempoolInfo } from '../../support/websocket';
const baseModule = Cypress.env("BASE_MODULE"); const baseModule = Cypress.env('BASE_MODULE');
describe('Testnet', () => { describe('Testnet', () => {
beforeEach(() => { beforeEach(() => {
@ -25,7 +25,7 @@ describe('Testnet', () => {
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit("/testnet"); cy.visit('/testnet');
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');

View File

@ -1,13 +0,0 @@
const fs = require('fs');
const CONFIG_FILE = 'mempool-frontend-config.json';
module.exports = (on, config) => {
if (fs.existsSync(CONFIG_FILE)) {
let contents = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
config.env.BASE_MODULE = contents.BASE_MODULE ? contents.BASE_MODULE : 'mempool';
} else {
config.env.BASE_MODULE = 'mempool';
}
return config;
}

View File

@ -58,7 +58,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.4.0", "@cypress/schematic": "^2.4.0",
"cypress": "^12.3.0", "cypress": "^12.7.0",
"cypress-fail-on-console-error": "~4.0.2", "cypress-fail-on-console-error": "~4.0.2",
"cypress-wait-until": "^1.7.2", "cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5", "mock-socket": "~9.1.5",
@ -7010,9 +7010,9 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "12.3.0", "version": "12.7.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.7.0.tgz",
"integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==", "integrity": "sha512-7rq+nmhzz0u6yabCFyPtADU2OOrYt6pvUau9qV7xyifJ/hnsaw/vkr0tnLlcuuQKUAOC1v1M1e4Z0zG7S0IAvA==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -7033,7 +7033,7 @@
"commander": "^5.1.0", "commander": "^5.1.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.2", "debug": "^4.3.4",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "6.4.7", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
@ -7159,6 +7159,23 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/cypress/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/cypress/node_modules/execa": { "node_modules/cypress/node_modules/execa": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
@ -22276,9 +22293,9 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "12.3.0", "version": "12.7.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.7.0.tgz",
"integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==", "integrity": "sha512-7rq+nmhzz0u6yabCFyPtADU2OOrYt6pvUau9qV7xyifJ/hnsaw/vkr0tnLlcuuQKUAOC1v1M1e4Z0zG7S0IAvA==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^2.88.10", "@cypress/request": "^2.88.10",
@ -22298,7 +22315,7 @@
"commander": "^5.1.0", "commander": "^5.1.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.2", "debug": "^4.3.4",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "6.4.7", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
@ -22382,6 +22399,15 @@
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"optional": true "optional": true
}, },
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"requires": {
"ms": "2.1.2"
}
},
"execa": { "execa": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",

View File

@ -110,7 +110,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.4.0", "@cypress/schematic": "^2.4.0",
"cypress": "^12.3.0", "cypress": "^12.7.0",
"cypress-fail-on-console-error": "~4.0.2", "cypress-fail-on-console-error": "~4.0.2",
"cypress-wait-until": "^1.7.2", "cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5", "mock-socket": "~9.1.5",

View File

@ -24,7 +24,7 @@
<td> <td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -17,7 +17,7 @@
<tbody *ngIf="blocks.value; else loadingTmpl"> <tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn"> <tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td> <td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td> <td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr> </tr>

View File

@ -35,7 +35,7 @@
<td> <td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -37,7 +37,7 @@
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span> {{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template> </ng-template>
</td> </td>
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td> <td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td> <td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr> </tr>
</tbody> </tbody>

View File

@ -3,13 +3,15 @@
{{ addPlus && satoshis >= 0 ? '+' : '' }} {{ addPlus && satoshis >= 0 ? '+' : '' }}
{{ {{
( (
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 (blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency ) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
}} }}
</span> </span>
<ng-template #noblockconversion> <ng-template #noblockconversion>
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span> <span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}
{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
</span>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@ -16,7 +16,7 @@
</tr> </tr>
<tr> <tr>
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td> <td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
<td><app-amount [blockConversion]="blockConversion" [satoshis]="value"></app-amount></td> <td><app-amount [blockConversion]="blockConversion" [satoshis]="value" [noFiat]="true"></app-amount></td>
</tr> </tr>
<tr> <tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td> <td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>

View File

@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@ -54,31 +54,6 @@
max-height: 270px; max-height: 270px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 1130px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.disabled { .disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@ -47,7 +47,7 @@
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template> <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div> </div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference"> <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
</div> </div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary" <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"

View File

@ -43,7 +43,7 @@
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td> </td>
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a <a

View File

@ -13,7 +13,7 @@
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height <td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
}}</a></td> }}</a></td>
<td class="text-left"> <td class="text-left">
<app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since> <app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td> <td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'"> <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">

View File

@ -10,7 +10,7 @@
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div> </div>
<div class="symbol"><app-time-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div> <div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5> <h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
@ -53,7 +53,7 @@
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div> </div>
<div class="symbol"><app-time-until [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time-until></div> <div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,9 @@
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu" <div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu">
style="padding: 0px 35px;">
<a routerLinkActive="active" class="btn btn-primary mr-1" [class]="padding" <a routerLinkActive="active" class="btn btn-primary" [class]="padding"
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a> [routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
<div ngbDropdown class="mr-1" [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD"> <div ngbDropdown [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD">
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button> <button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1"> <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]" <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"

View File

@ -1,6 +1,15 @@
.menu { .menu {
flex-grow: 1; flex-grow: 1;
padding: 0 35px;
@media (min-width: 576px) { @media (min-width: 576px) {
max-width: 400px; max-width: 400px;
} }
& > * {
margin: 0;
margin-inline-end: 0.25rem;
&.last-child {
margin-inline-end: 0;
}
}
} }

View File

@ -54,31 +54,6 @@
height: 240px; height: 240px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.pool-distribution { .pool-distribution {
min-height: 56px; min-height: 56px;
display: block; display: block;

View File

@ -48,31 +48,6 @@
max-height: 293px; max-height: 293px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@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 { .loadingGraphs {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@ -23,10 +23,10 @@
</div> </div>
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock"> <div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet"> <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until> <app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template> </ng-template>
<ng-template #timeDiffMainnet> <ng-template #timeDiffMainnet>
<app-time-until [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until> <app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template> </ng-template>
</div> </div>
<ng-template #mergedBlock> <ng-template #mergedBlock>

View File

@ -33,31 +33,6 @@
} }
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.bottom-padding { .bottom-padding {
@media (max-width: 992px) { @media (max-width: 992px) {
padding-bottom: 65px padding-bottom: 65px

View File

@ -227,7 +227,7 @@
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
</td> </td>
<td class="mined"> <td class="mined">
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
</td> </td>
<td class="coinbase"> <td class="coinbase">
<span class="badge badge-secondary scriptmessage longer"> <span class="badge badge-secondary scriptmessage longer">

View File

@ -107,7 +107,13 @@ export class SearchFormComponent implements OnInit {
}))), }))),
); );
}), }),
tap((result: any[]) => { map((result: any[]) => {
if (this.network === 'bisq') {
result[0] = result[0].map((address: string) => 'B' + address);
}
return result;
}),
tap(() => {
this.isTypeaheading$.next(false); this.isTypeaheading$.next(false);
}) })
); );
@ -126,7 +132,7 @@ export class SearchFormComponent implements OnInit {
] ]
).pipe( ).pipe(
map((latestData) => { map((latestData) => {
const searchText = latestData[0]; let searchText = latestData[0];
if (!searchText.length) { if (!searchText.length) {
return { return {
searchText: '', searchText: '',
@ -144,15 +150,15 @@ export class SearchFormComponent implements OnInit {
const addressPrefixSearchResults = result[0]; const addressPrefixSearchResults = result[0];
const lightningResults = result[1]; const lightningResults = result[1];
if (this.network === 'bisq') {
return searchText.map((address: string) => 'B' + address);
}
const matchesBlockHeight = this.regexBlockheight.test(searchText); const matchesBlockHeight = this.regexBlockheight.test(searchText);
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText); const matchesBlockHash = this.regexBlockhash.test(searchText);
const matchesAddress = this.regexAddress.test(searchText); const matchesAddress = this.regexAddress.test(searchText);
if (matchesAddress && this.network === 'bisq') {
searchText = 'B' + searchText;
}
return { return {
searchText: searchText, searchText: searchText,
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress), hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),

View File

@ -11,6 +11,8 @@
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr"> <div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr">
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer <div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
(mousedown)="onMouseDown($event)" (mousedown)="onMouseDown($event)"
(pointerdown)="onPointerDown($event)"
(touchmove)="onTouchMove($event)"
(dragstart)="onDragStart($event)" (dragstart)="onDragStart($event)"
(scroll)="onScroll($event)" (scroll)="onScroll($event)"
> >

View File

@ -27,6 +27,7 @@ export class StartComponent implements OnInit, OnDestroy {
@ViewChild('blockchainContainer') blockchainContainer: ElementRef; @ViewChild('blockchainContainer') blockchainContainer: ElementRef;
isMobile: boolean = false; isMobile: boolean = false;
isiOS: boolean = false;
blockWidth = 155; blockWidth = 155;
dynamicBlocksAmount: number = 8; dynamicBlocksAmount: number = 8;
blockCount: number = 0; blockCount: number = 0;
@ -37,10 +38,15 @@ export class StartComponent implements OnInit, OnDestroy {
pageIndex: number = 0; pageIndex: number = 0;
pages: any[] = []; pages: any[] = [];
pendingMark: number | void = null; pendingMark: number | void = null;
lastUpdate: number = 0;
lastMouseX: number;
velocity: number = 0;
constructor( constructor(
private stateService: StateService, private stateService: StateService,
) { } ) {
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
}
ngOnInit() { ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount); this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
@ -133,17 +139,31 @@ export class StartComponent implements OnInit, OnDestroy {
onMouseDown(event: MouseEvent) { onMouseDown(event: MouseEvent) {
this.mouseDragStartX = event.clientX; this.mouseDragStartX = event.clientX;
this.resetMomentum(event.clientX);
this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft;
} }
onPointerDown(event: PointerEvent) {
if (this.isiOS) {
event.preventDefault();
this.onMouseDown(event);
}
}
onDragStart(event: MouseEvent) { // Ignore Firefox annoying default drag behavior onDragStart(event: MouseEvent) { // Ignore Firefox annoying default drag behavior
event.preventDefault(); event.preventDefault();
} }
onTouchMove(event: TouchEvent) {
// disable native scrolling on iOS
if (this.isiOS) {
event.preventDefault();
}
}
// We're catching the whole page event here because we still want to scroll blocks // We're catching the whole page event here because we still want to scroll blocks
// even if the mouse leave the blockchain blocks container. Same idea for mouseup below. // even if the mouse leave the blockchain blocks container. Same idea for mouseup below.
@HostListener('document:mousemove', ['$event']) @HostListener('document:mousemove', ['$event'])
onMouseMove(event: MouseEvent): void { onMouseMove(event: MouseEvent): void {
if (this.mouseDragStartX != null) { if (this.mouseDragStartX != null) {
this.updateVelocity(event.clientX);
this.stateService.setBlockScrollingInProgress(true); this.stateService.setBlockScrollingInProgress(true);
this.blockchainContainer.nativeElement.scrollLeft = this.blockchainContainer.nativeElement.scrollLeft =
this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX; this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX;
@ -152,7 +172,60 @@ export class StartComponent implements OnInit, OnDestroy {
@HostListener('document:mouseup', []) @HostListener('document:mouseup', [])
onMouseUp() { onMouseUp() {
this.mouseDragStartX = null; this.mouseDragStartX = null;
this.stateService.setBlockScrollingInProgress(false); this.animateMomentum();
}
@HostListener('document:pointermove', ['$event'])
onPointerMove(event: PointerEvent): void {
if (this.isiOS) {
this.onMouseMove(event);
}
}
@HostListener('document:pointerup', [])
@HostListener('document:pointercancel', [])
onPointerUp() {
if (this.isiOS) {
this.onMouseUp();
}
}
resetMomentum(x: number) {
this.lastUpdate = performance.now();
this.lastMouseX = x;
this.velocity = 0;
}
updateVelocity(x: number) {
const now = performance.now();
let dt = now - this.lastUpdate;
if (dt > 0) {
this.lastUpdate = now;
const velocity = (x - this.lastMouseX) / dt;
this.velocity = (0.8 * this.velocity) + (0.2 * velocity);
this.lastMouseX = x;
}
}
animateMomentum() {
this.lastUpdate = performance.now();
requestAnimationFrame(() => {
const now = performance.now();
const dt = now - this.lastUpdate;
this.lastUpdate = now;
if (Math.abs(this.velocity) < 0.005) {
this.stateService.setBlockScrollingInProgress(false);
} else {
const deceleration = Math.max(0.0025, 0.001 * this.velocity * this.velocity) * (this.velocity > 0 ? -1 : 1);
const displacement = (this.velocity * dt) - (0.5 * (deceleration * dt * dt));
const dv = (deceleration * dt);
if ((this.velocity < 0 && dv + this.velocity > 0) || (this.velocity > 0 && dv + this.velocity < 0)) {
this.velocity = 0;
} else {
this.velocity += dv;
}
this.blockchainContainer.nativeElement.scrollLeft -= displacement;
this.animateMomentum();
}
});
} }
onScroll(e) { onScroll(e) {

View File

@ -49,6 +49,9 @@
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y <input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
</label> </label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
</label>
</div> </div>
<div class="small-buttons"> <div class="small-buttons">
<div ngbDropdown #myDrop="ngbDropdown"> <div ngbDropdown #myDrop="ngbDropdown">

View File

@ -70,7 +70,7 @@ export class StatisticsComponent implements OnInit {
this.route this.route
.fragment .fragment
.subscribe((fragment) => { .subscribe((fragment) => {
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y'].indexOf(fragment) > -1) { if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} }
}); });
@ -109,7 +109,10 @@ export class StatisticsComponent implements OnInit {
if (this.radioGroupForm.controls.dateSpan.value === '2y') { if (this.radioGroupForm.controls.dateSpan.value === '2y') {
return this.apiService.list2YStatistics$(); return this.apiService.list2YStatistics$();
} }
return this.apiService.list3YStatistics$(); if (this.radioGroupForm.controls.dateSpan.value === '3y') {
return this.apiService.list3YStatistics$();
}
return this.apiService.list4YStatistics$();
}) })
) )
.subscribe((mempoolStats: any) => { .subscribe((mempoolStats: any) => {
@ -181,7 +184,7 @@ export class StatisticsComponent implements OnInit {
} }
let capRatio = 10; let capRatio = 10;
if (['1m', '3m', '6m', '1y', '2y', '3y'].includes(this.graphWindowPreference)) { if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
capRatio = 4; capRatio = 4;
} }

View File

@ -25,6 +25,12 @@
</defs> </defs>
</svg> </svg>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'warning'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/>
<path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'mempoolSpace'"> <ng-container *ngSwitchCase="'mempoolSpace'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/> <path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/>

View File

@ -13,4 +13,5 @@ export class SvgImagesComponent {
@Input() width: string; @Input() width: string;
@Input() height: string; @Input() height: string;
@Input() viewBox: string; @Input() viewBox: string;
@Input() fill: string;
} }

View File

@ -1,98 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-since',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() dateString: number;
@Input() fastRender = false;
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
let date: Date;
if (this.dateString) {
date = new Date(this.dateString)
} else {
date = new Date(this.time * 1000);
}
const seconds = Math.floor((+new Date() - +date) / 1000);
if (seconds < 60) {
return $localize`:@@date-base.just-now:Just now`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
counter = Math.floor(seconds / this.intervals[i]);
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
}
}
}
}
}
}
}

View File

@ -1,91 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-span',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeSpanComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() fastRender = false;
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
const seconds = Math.floor(this.time);
if (seconds < 60) {
return $localize`:@@date-base.just-now:Just now`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
counter = Math.floor(seconds / this.intervals[i]);
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
}
}
}
}
}
}
}

View File

@ -1,104 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time-until',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeUntilComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() forceFloorOnTimeIntervals: string[];
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if(this.fixedRender){
this.text = this.calculate();
return;
}
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
const seconds = (+new Date(this.time) - +new Date()) / 1000;
if (seconds < 60) {
const dateStrings = dates(1);
return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
}
let counter: number;
for (const i in this.intervals) {
if (this.intervals.hasOwnProperty(i)) {
if (this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
counter = Math.floor(seconds / this.intervals[i]);
} else {
counter = Math.round(seconds / this.intervals[i]);
}
const dateStrings = dates(counter);
if (counter > 0) {
if (counter === 1) {
switch (i) { // singular (In ~1 day)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
}
} else {
switch (i) { // plural (In ~2 days)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
}
}
}
}
}
}
}

View File

@ -0,0 +1,190 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
@Component({
selector: 'app-time',
template: `{{ text }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
intervals = {};
@Input() time: number;
@Input() dateString: number;
@Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain';
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() relative = false;
@Input() forceFloorOnTimeIntervals: string[];
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
) {
this.intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
};
}
ngOnInit() {
if(this.fixedRender){
this.text = this.calculate();
return;
}
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => {
this.text = this.calculate();
this.ref.markForCheck();
}, 1000 * (this.fastRender ? 1 : 60));
}
ngOnChanges() {
this.text = this.calculate();
this.ref.markForCheck();
}
ngOnDestroy() {
clearInterval(this.interval);
}
calculate() {
let seconds: number;
switch (this.kind) {
case 'since':
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
break;
case 'until':
seconds = (+new Date(this.time) - +new Date()) / 1000;
break;
default:
seconds = Math.floor(this.time);
}
if (seconds < 60) {
if (this.relative || this.kind === 'since') {
return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until') {
seconds = 60;
}
}
let counter: number;
for (const i in this.intervals) {
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
counter = Math.floor(seconds / this.intervals[i]);
} else {
counter = Math.round(seconds / this.intervals[i]);
}
const dateStrings = dates(counter);
if (counter > 0) {
switch (this.kind) {
case 'since':
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
}
}
break;
case 'until':
if (counter === 1) {
switch (i) { // singular (In ~1 day)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
}
} else {
switch (i) { // plural (In ~2 days)
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
}
}
break;
case 'span':
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
}
}
break;
default:
if (counter === 1) {
switch (i) { // singular (1 day)
case 'year': return dateStrings.i18nYear; break;
case 'month': return dateStrings.i18nMonth; break;
case 'week': return dateStrings.i18nWeek; break;
case 'day': return dateStrings.i18nDay; break;
case 'hour': return dateStrings.i18nHour; break;
case 'minute': return dateStrings.i18nMinute; break;
case 'second': return dateStrings.i18nSecond; break;
}
} else {
switch (i) { // plural (2 days)
case 'year': return dateStrings.i18nYears; break;
case 'month': return dateStrings.i18nMonths; break;
case 'week': return dateStrings.i18nWeeks; break;
case 'day': return dateStrings.i18nDays; break;
case 'hour': return dateStrings.i18nHours; break;
case 'minute': return dateStrings.i18nMinutes; break;
case 'second': return dateStrings.i18nSeconds; break;
}
}
}
}
}
}
}

View File

@ -57,14 +57,14 @@
<td> <td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="tx.status.block_time" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div> </div>
</td> </td>
</tr> </tr>
<ng-template [ngIf]="transactionTime > 0"> <ng-template [ngIf]="transactionTime > 0">
<tr> <tr>
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td> <td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
<td><app-time-span [time]="tx.status.block_time - transactionTime" [fastRender]="true"></app-time-span></td> <td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
</tr> </tr>
</ng-template> </ng-template>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
@ -100,7 +100,7 @@
<ng-template #firstSeenTmpl> <ng-template #firstSeenTmpl>
<tr> <tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td> <td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since></i></td> <td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
</tr> </tr>
</ng-template> </ng-template>
</ng-template> </ng-template>
@ -116,10 +116,10 @@
</ng-template> </ng-template>
<ng-template #belowBlockLimit> <ng-template #belowBlockLimit>
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault"> <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
<app-time-until [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time-until> <app-time kind="until" [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template> </ng-template>
<ng-template #timeEstimateDefault> <ng-template #timeEstimateDefault>
<app-time-until *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until> <app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
</ng-template> </ng-template>
</ng-template> </ng-template>
</ng-template> </ng-template>
@ -210,6 +210,7 @@
<div class="graph-container" #graphContainer> <div class="graph-container" #graphContainer>
<tx-bowtie-graph <tx-bowtie-graph
[tx]="tx" [tx]="tx"
[cached]="isCached"
[width]="graphWidth" [width]="graphWidth"
[height]="graphHeight" [height]="graphHeight"
[lineLimit]="inOutLimit" [lineLimit]="inOutLimit"
@ -250,7 +251,7 @@
</div> </div>
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list> <app-transactions-list #txList [transactions]="[tx]" [cached]="isCached" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
<div class="title text-left"> <div class="title text-left">
<h2 i18n="transaction.details">Details</h2> <h2 i18n="transaction.details">Details</h2>

View File

@ -204,6 +204,12 @@
.txids { .txids {
width: 60%; width: 60%;
} }
@media (max-width: 500px) {
.txids {
width: 40%;
}
}
} }
.tx-list { .tx-list {

View File

@ -57,6 +57,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchCpfp$ = new Subject<string>(); fetchCpfp$ = new Subject<string>();
fetchRbfHistory$ = new Subject<string>(); fetchRbfHistory$ = new Subject<string>();
fetchCachedTx$ = new Subject<string>(); fetchCachedTx$ = new Subject<string>();
isCached: boolean = false;
now = new Date().getTime(); now = new Date().getTime();
timeAvg$: Observable<number>; timeAvg$: Observable<number>;
liquidUnblinding = new LiquidUnblinding(); liquidUnblinding = new LiquidUnblinding();
@ -196,6 +197,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
this.tx = tx; this.tx = tx;
this.isCached = true;
if (tx.fee === undefined) { if (tx.fee === undefined) {
this.tx.fee = 0; this.tx.fee = 0;
} }
@ -289,6 +291,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} }
this.tx = tx; this.tx = tx;
this.isCached = false;
if (tx.fee === undefined) { if (tx.fee === undefined) {
this.tx.fee = 0; this.tx.fee = 0;
} }
@ -362,7 +365,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.waitingForTransaction = false; this.waitingForTransaction = false;
} }
this.rbfTransaction = rbfTransaction; this.rbfTransaction = rbfTransaction;
this.cacheService.setTxCache([this.rbfTransaction]);
this.replaced = true; this.replaced = true;
if (rbfTransaction && !this.tx) { if (rbfTransaction && !this.tx) {
this.fetchCachedTx$.next(this.txId); this.fetchCachedTx$.next(this.txId);

View File

@ -6,7 +6,7 @@
<div> <div>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template> <ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen"> <ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time-since [time]="tx.firstSeen" [fastRender]="true"></app-time-since></i> <i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true"></app-time></i>
</ng-template> </ng-template>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service'; import { CacheService } from '../../services/cache.service';
import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription, of } from 'rxjs';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@ -23,6 +23,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
showMoreIncrement = 1000; showMoreIncrement = 1000;
@Input() transactions: Transaction[]; @Input() transactions: Transaction[];
@Input() cached: boolean = false;
@Input() showConfirmations = false; @Input() showConfirmations = false;
@Input() transactionPage = false; @Input() transactionPage = false;
@Input() errorUnblinded = false; @Input() errorUnblinded = false;
@ -67,7 +68,13 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.outspendsSubscription = merge( this.outspendsSubscription = merge(
this.refreshOutspends$ this.refreshOutspends$
.pipe( .pipe(
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)), switchMap((txIds) => {
if (!this.cached) {
return this.apiService.getOutspendsBatched$(txIds);
} else {
return of([]);
}
}),
tap((outspends: Outspend[][]) => { tap((outspends: Outspend[][]) => {
if (!this.transactions) { if (!this.transactions) {
return; return;
@ -155,7 +162,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
).subscribe(); ).subscribe();
}); });
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid); const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
if (txIds.length) { if (txIds.length && !this.cached) {
this.refreshOutspends$.next(txIds); this.refreshOutspends$.next(txIds);
} }
if (this.stateService.env.LIGHTNING) { if (this.stateService.env.LIGHTNING) {

View File

@ -2,7 +2,7 @@ import { Component, OnInit, Input, OnChanges, HostListener, Inject, LOCALE_ID }
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Outspend, Transaction } from '../../interfaces/electrs.interface'; import { Outspend, Transaction } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription } from 'rxjs'; import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators'; import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@ -40,6 +40,7 @@ interface Xput {
export class TxBowtieGraphComponent implements OnInit, OnChanges { export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() network: string; @Input() network: string;
@Input() cached: boolean = false;
@Input() width = 1200; @Input() width = 1200;
@Input() height = 600; @Input() height = 600;
@Input() lineLimit = 250; @Input() lineLimit = 250;
@ -107,7 +108,13 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
this.outspendsSubscription = merge( this.outspendsSubscription = merge(
this.refreshOutspends$ this.refreshOutspends$
.pipe( .pipe(
switchMap((txid) => this.apiService.getOutspendsBatched$([txid])), switchMap((txid) => {
if (!this.cached) {
return this.apiService.getOutspendsBatched$([txid]);
} else {
return of(null);
}
}),
tap((outspends: Outspend[][]) => { tap((outspends: Outspend[][]) => {
if (!this.tx || !outspends || !outspends.length) { if (!this.tx || !outspends || !outspends.length) {
return; return;
@ -132,7 +139,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
ngOnChanges(): void { ngOnChanges(): void {
this.initGraph(); this.initGraph();
this.refreshOutspends$.next(this.tx.txid); if (!this.cached) {
this.refreshOutspends$.next(this.tx.txid);
}
} }
initGraph(): void { initGraph(): void {

View File

@ -93,7 +93,7 @@
<tbody> <tbody>
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock"> <tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td> <td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4"> <td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> <a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}" <img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
@ -250,7 +250,7 @@
</span> </span>
<ng-template #inSync> <ng-template #inSync>
<div class="progress inc-tx-progress-bar"> <div class="progress inc-tx-progress-bar">
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth, 'background-color': mempoolInfoData.value.progressColor}">&nbsp;</div> <div class="progress-bar {{ mempoolInfoData.value.progressColor }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">&nbsp;</div>
<div class="progress-text">&lrm;{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div> <div class="progress-text">&lrm;{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
</div> </div>
</ng-template> </ng-template>

View File

@ -78,21 +78,12 @@ export class DashboardComponent implements OnInit, OnDestroy {
map(([mempoolInfo, vbytesPerSecond]) => { map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100); const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressColor = '#7CB342'; let progressColor = 'bg-success';
if (vbytesPerSecond > 1667) { if (vbytesPerSecond > 1667) {
progressColor = '#FDD835'; progressColor = 'bg-warning';
}
if (vbytesPerSecond > 2000) {
progressColor = '#FFB300';
}
if (vbytesPerSecond > 2500) {
progressColor = '#FB8C00';
} }
if (vbytesPerSecond > 3000) { if (vbytesPerSecond > 3000) {
progressColor = '#F4511E'; progressColor = 'bg-danger';
}
if (vbytesPerSecond > 3500) {
progressColor = '#D81B60';
} }
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100); const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);

View File

@ -10,7 +10,8 @@
<div class="doc-content"> <div class="doc-content">
<div id="disclaimer"> <div id="disclaimer">
<table><tr><td><svg viewBox="0 0 304 304" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd" style="fill:#ffc107;fill-opacity:1"><path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/><path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/></g></svg></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table> <table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
</div> </div>

View File

@ -274,10 +274,8 @@ h3 {
margin: 24px 0; margin: 24px 0;
} }
#disclaimer svg { .disclaimer-warning {
width: 50px; margin-right: 50px;
height: auto;
margin-right: 32px;
} }
#disclaimer p:last-child { #disclaimer p:last-child {
@ -294,6 +292,12 @@ h3 {
display: none; display: none;
} }
.disclaimer-warning {
display: block;
margin: 2px auto 16px;
text-align: center;
}
.doc-content { .doc-content {
width: 100%; width: 100%;
float: unset; float: unset;
@ -332,6 +336,10 @@ h3 {
.doc-welcome-note { .doc-welcome-note {
font-size: 0.85rem; font-size: 0.85rem;
} }
#disclaimer table {
display: none;
}
} }
@media (min-width: 992px) { @media (min-width: 992px) {

View File

@ -29,6 +29,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
screenWidth: number; screenWidth: number;
officialMempoolInstance: boolean; officialMempoolInstance: boolean;
auditEnabled: boolean; auditEnabled: boolean;
mobileViewport: boolean = false;
@ViewChildren(FaqTemplateDirective) faqTemplates: QueryList<FaqTemplateDirective>; @ViewChildren(FaqTemplateDirective) faqTemplates: QueryList<FaqTemplateDirective>;
dict = {}; dict = {};
@ -43,6 +44,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
this.faqTemplates.forEach((x) => this.dict[x.type] = x.template); this.faqTemplates.forEach((x) => this.dict[x.type] = x.template);
} }
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative"; this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
this.mobileViewport = window.innerWidth <= 992;
} }
ngAfterViewInit() { ngAfterViewInit() {

View File

@ -1,14 +1,14 @@
<span class="green-color" *ngIf="blockConversion; else noblockconversion"> <span class="green-color" *ngIf="blockConversion; else noblockconversion">
{{ {{
( (
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ?? (blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0 (blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
) * value / 100000000 | fiatCurrency : digitsInfo : currency ) * value / 100000000 | fiatCurrency : digitsInfo : currency
}} }}
</span> </span>
<ng-template #noblockconversion> <ng-template #noblockconversion>
<span class="green-color" *ngIf="(conversions$ | async) as conversions"> <span class="green-color" *ngIf="(conversions$ | async) as conversions">
{{ (conversions[currency] ?? conversions['USD'] ?? 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }} {{ (conversions[currency] > -1 ? conversions[currency] : 0) * value / 100000000 | fiatCurrency : digitsInfo : currency }}
</span> </span>
</ng-template> </ng-template>

View File

@ -20,7 +20,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px"> <div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px">
<span class="text-center" i18n="lightning.channel-not-found">No channel found for short id "{{ channel.short_id }}"</span> <span class="text-center">No channel found for ID "{{ channel.short_id }}"</span>
</div> </div>
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" <app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'"

View File

@ -78,5 +78,5 @@ h3 {
.details-button { .details-button {
align-self: center; align-self: center;
margin-left: auto; margin-inline-start: auto;
} }

View File

@ -19,6 +19,14 @@
} }
} }
:host-context(.rtl-layout) .formRadioGroup {
direction: ltr;
@media (min-width: 435px) {
right: unset;
left: 0;
}
}
.btn-group { .btn-group {
@media (max-width: 435px) { @media (max-width: 435px) {
flex-grow: 1; flex-grow: 1;

View File

@ -92,7 +92,7 @@
</ng-template> </ng-template>
<input type="text" class="form-control" aria-label="Text input with dropdown button" <input type="text" class="form-control" aria-label="Text input with dropdown button"
[value]="node.socketsObject[selectedSocketIndex].socket"> [value]="node.socketsObject[selectedSocketIndex].socket">
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04" (mouseover)="qrCodeVisible[i] = 1" <button class="btn btn-secondary" type="button" id="inputGroupFileAddon04" (mouseover)="qrCodeVisible[i] = 1"
(mouseout)="qrCodeVisible[i] = 0"> (mouseout)="qrCodeVisible[i] = 0">
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true"></fa-icon>
<div class="qr-wrapper" [hidden]="!qrCodeVisible[i]"> <div class="qr-wrapper" [hidden]="!qrCodeVisible[i]">

View File

@ -8,7 +8,7 @@
} }
h1 { h1 {
margin-left: 15px; margin-inline-start: 15px;
} }
.qr-wrapper { .qr-wrapper {
@ -57,3 +57,17 @@ h1 {
.description-text { .description-text {
white-space: break-spaces; white-space: break-spaces;
} }
.timestamp-first .input-group {
input {
margin-inline-end: .25rem;
}
}
:host-context(.rtl-layout) {
.timestamp-first .input-group {
button {
margin-inline-end: .25rem;
}
}
}

View File

@ -17,7 +17,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px"> <div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px">
<span class="text-center" i18n="lightning.node-not-found">No node found for public key "{{ node.public_key | shortenString : 12}}"</span> <span class="text-center">No node found for public key "{{ node.public_key | shortenString : 12}}"</span>
</div> </div>
<div class="box" *ngIf="!error"> <div class="box" *ngIf="!error">
@ -57,7 +57,7 @@
</tr> </tr>
<tr *ngIf="(avgChannelDistance$ | async) as avgDistance;"> <tr *ngIf="(avgChannelDistance$ | async) as avgDistance;">
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td> <td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
<td>{{ avgDistance | number : '1.0-0' }} <span class="symbol">km</span> <span class="separator">/</span> {{ kmToMiles(avgDistance) | number : '1.0-0' }} <span class="symbol">mi</span></td> <td class="direction-ltr">{{ avgDistance | number : '1.0-0' }} <span class="symbol">km</span> <span class="separator">/</span> {{ kmToMiles(avgDistance) | number : '1.0-0' }} <span class="symbol">mi</span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -4,10 +4,6 @@
&.widget { &.widget {
height: 250px; height: 250px;
} }
&.graph {
height: auto;
}
} }
.card-header { .card-header {

View File

@ -229,6 +229,7 @@ export class NodesChannelsMap implements OnInit {
title: title ?? undefined, title: title ?? undefined,
tooltip: {}, tooltip: {},
geo: { geo: {
top: 75,
animation: false, animation: false,
silent: true, silent: true,
center: this.center, center: this.center,

View File

@ -53,31 +53,6 @@
height: 145px; height: 145px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.pool-distribution { .pool-distribution {
min-height: 56px; min-height: 56px;
display: block; display: block;

View File

@ -182,7 +182,7 @@ export class NodesNetworksChartComponent implements OnInit {
{ {
zlevel: 1, zlevel: 1,
yAxisIndex: 0, yAxisIndex: 0,
name: $localize`Clearnet (IPv4, IPv6)`, name: $localize`Clearnet Only (IPv4, IPv6)`,
showSymbol: false, showSymbol: false,
symbol: 'none', symbol: 'none',
data: data.clearnet_nodes, data: data.clearnet_nodes,
@ -292,7 +292,7 @@ export class NodesNetworksChartComponent implements OnInit {
icon: 'roundRect', icon: 'roundRect',
}, },
{ {
name: $localize`Clearnet (IPv4, IPv6)`, name: $localize`Clearnet Only (IPv4, IPv6)`,
inactiveColor: 'rgb(110, 112, 121)', inactiveColor: 'rgb(110, 112, 121)',
textStyle: { textStyle: {
color: 'white', color: 'white',
@ -318,7 +318,7 @@ export class NodesNetworksChartComponent implements OnInit {
], ],
selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? {
'$localize`Darknet Only (Tor, I2P, cjdns)`': true, '$localize`Darknet Only (Tor, I2P, cjdns)`': true,
'$localize`Clearnet (IPv4, IPv6)`': true, '$localize`Clearnet Only (IPv4, IPv6)`': true,
'$localize`Clearnet and Darknet`': true, '$localize`Clearnet and Darknet`': true,
'$localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`': true, '$localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`': true,
} }

View File

@ -34,31 +34,6 @@
} }
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.bottom-padding { .bottom-padding {
@media (max-width: 992px) { @media (max-width: 992px) {
padding-bottom: 65px padding-bottom: 65px

View File

@ -11,7 +11,7 @@
<th class="pool text-left" i18n="nodes.alias" [ngClass]="{'widget': widget}">Alias</th> <th class="pool text-left" i18n="nodes.alias" [ngClass]="{'widget': widget}">Alias</th>
<th class="liquidity text-right" i18n="node.channels">Channels</th> <th class="liquidity text-right" i18n="node.channels">Channels</th>
<th *ngIf="!widget" class="d-none d-md-table-cell channels text-right" i18n="lightning.channels">Capacity</th> <th *ngIf="!widget" class="d-none d-md-table-cell channels text-right" i18n="lightning.channels">Capacity</th>
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="node.liquidity">{{ currency$ | async }}</th> <th *ngIf="!widget" class="d-none d-md-table-cell text-right">{{ currency$ | async }}</th>
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="transaction.first-seen|Transaction first seen">First seen</th> <th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="transaction.first-seen|Transaction first seen">First seen</th>
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th> <th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>

View File

@ -53,31 +53,6 @@
height: 145px; height: 145px;
} }
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -100px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
.pool-distribution { .pool-distribution {
min-height: 56px; min-height: 56px;
display: block; display: block;

View File

@ -68,6 +68,10 @@ export class ApiService {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/3y'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/3y');
} }
list4YStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/4y');
}
getTransactionTimes$(txIds: string[]): Observable<number[]> { getTransactionTimes$(txIds: string[]): Observable<number[]> {
let params = new HttpParams(); let params = new HttpParams();
txIds.forEach((txId: string) => { txIds.forEach((txId: string) => {

View File

@ -89,7 +89,7 @@ export class PriceService {
return this.singlePriceObservable$.pipe( return this.singlePriceObservable$.pipe(
map((conversion) => { map((conversion) => {
if (conversion.prices.length <= 0) { if (conversion.prices.length <= 0) {
return this.getEmptyPrice(); return undefined;
} }
return { return {
price: { price: {
@ -113,7 +113,7 @@ export class PriceService {
return this.priceObservable$.pipe( return this.priceObservable$.pipe(
map((conversion) => { map((conversion) => {
if (!blockTimestamp) { if (!blockTimestamp || !conversion) {
return undefined; return undefined;
} }

View File

@ -2,6 +2,6 @@
<span *ngIf="seconds !== undefined"> <span *ngIf="seconds !== undefined">
&lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} &lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
<div class="lg-inline" *ngIf="!hideTimeSince"> <div class="lg-inline" *ngIf="!hideTimeSince">
<i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i> <i class="symbol">(<app-time kind="since" [time]="seconds" [fastRender]="true"></app-time>)</i>
</div> </div>
</span> </span>

View File

@ -21,6 +21,7 @@ export const formatterXAxis = (
return date.toLocaleTimeString(locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); return date.toLocaleTimeString(locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
case '2y': case '2y':
case '3y': case '3y':
case '4y':
case 'all': case 'all':
return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }); return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' });
} }
@ -45,6 +46,7 @@ export const formatterXAxisLabel = (
case '1y': case '1y':
case '2y': case '2y':
case '3y': case '3y':
case '4y':
return null; return null;
} }
}; };
@ -71,6 +73,7 @@ export const formatterXAxisTimeCategory = (
return date.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric' }); return date.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric' });
case '2y': case '2y':
case '3y': case '3y':
case '4y':
case 'all': case 'all':
return date.toLocaleDateString(locale, { year: 'numeric', month: 'long' }); return date.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
} }

View File

@ -23,6 +23,10 @@ export class FiatCurrencyPipe implements PipeTransform {
const digits = args[0] || 1; const digits = args[0] || 1;
const currency = args[1] || this.currency || 'USD'; const currency = args[1] || this.currency || 'USD';
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num); if (num >= 1000) {
return new Intl.NumberFormat(this.locale, { style: 'currency', currency, maximumFractionDigits: 0 }).format(num);
} else {
return new Intl.NumberFormat(this.locale, { style: 'currency', currency }).format(num);
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More