Merge branch 'master' into mononaut/seo-ssr
This commit is contained in:
commit
a874cdfb56
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
backend/src/api/database-migration.ts @wiz @softsimon
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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())();
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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']) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
29
backend/src/utils/format.ts
Normal file
29
backend/src/utils/format.ts
Normal 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]}`;
|
||||||
|
}
|
@ -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__",
|
||||||
|
@ -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}',
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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');
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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;
|
|
||||||
}
|
|
44
frontend/package-lock.json
generated
44
frontend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ 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>
|
||||||
|
@ -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>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ 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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ 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
|
||||||
|
@ -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'">
|
||||||
|
@ -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>
|
||||||
|
@ -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]"
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
@ -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%;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -227,7 +227,7 @@
|
|||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ 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">
|
||||||
|
@ -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),
|
||||||
|
@ -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)"
|
||||||
>
|
>
|
||||||
|
@ -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) {
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"/>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
190
frontend/src/app/components/time/time.component.ts
Normal file
190
frontend/src/app/components/time/time.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -57,14 +57,14 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ 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>
|
||||||
|
@ -204,6 +204,12 @@
|
|||||||
.txids {
|
.txids {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.txids {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-list {
|
.tx-list {
|
||||||
|
@ -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);
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
|
<ng-template [ngIf]="tx.status.confirmed">‎{{ 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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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}"> </div>
|
<div class="progress-bar {{ mempoolInfoData.value.progressColor }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}"> </div>
|
||||||
<div class="progress-text">‎{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
|
<div class="progress-text">‎{{ 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>
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
@ -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'"
|
||||||
|
@ -78,5 +78,5 @@ h3 {
|
|||||||
|
|
||||||
.details-button {
|
.details-button {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-left: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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]">
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -4,10 +4,6 @@
|
|||||||
&.widget {
|
&.widget {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.graph {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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) => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
<span *ngIf="seconds !== undefined">
|
<span *ngIf="seconds !== undefined">
|
||||||
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
|
‎{{ 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>
|
||||||
|
@ -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' });
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user