Merge branch 'master' into nymkappa/bugfix/node-map

This commit is contained in:
wiz 2022-12-01 16:55:22 +09:00 committed by GitHub
commit 13b52c427c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 8382 additions and 9459 deletions

View File

@ -25,7 +25,9 @@
"AUTOMATIC_BLOCK_REINDEXING": false, "AUTOMATIC_BLOCK_REINDEXING": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json", "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"ADVANCED_TRANSACTION_SELECTION": false "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false,
"TRANSACTION_INDEXING": false
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View File

@ -26,7 +26,9 @@
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__", "POOLS_JSON_URL": "__POOLS_JSON_URL__",
"ADVANCED_TRANSACTION_SELECTION": "__ADVANCED_TRANSACTION_SELECTION__" "ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
"TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",

View File

@ -38,7 +38,9 @@ describe('Mempool Backend Config', () => {
STDOUT_LOG_MIN_PRIORITY: 'debug', STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
ADVANCED_TRANSACTION_SELECTION: false, ADVANCED_GBT_AUDIT: false,
ADVANCED_GBT_MEMPOOL: false,
TRANSACTION_INDEXING: false,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });

View File

@ -10,9 +10,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
class Audit { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
: { censored: string[], added: string[], score: number } { : { censored: string[], added: string[], fresh: string[], score: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], score: 0 }; return { censored: [], added: [], fresh: [], score: 0 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
@ -83,7 +83,17 @@ class Audit {
} else { } else {
if (!isDisplaced[tx.txid]) { if (!isDisplaced[tx.txid]) {
added.push(tx.txid); added.push(tx.txid);
} else {
} }
let blockIndex = -1;
let index = -1;
projectedBlocks.forEach((block, bi) => {
const i = block.transactionIds.indexOf(tx.txid);
if (i >= 0) {
blockIndex = bi;
index = i;
}
});
overflowWeight += tx.weight; overflowWeight += tx.weight;
} }
totalWeight += tx.weight; totalWeight += tx.weight;
@ -119,48 +129,10 @@ class Audit {
return { return {
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
fresh,
score score
}; };
} }
public async $getBlockAuditScores(fromHeight?: number, limit: number = 15): Promise<AuditScore[]> {
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
const returnScores: AuditScore[] = [];
if (currentHeight < 0) {
return returnScores;
}
for (let i = 0; i < limit && currentHeight >= 0; i++) {
const block = blocks.getBlocks().find((b) => b.height === currentHeight);
if (block?.extras?.matchRate != null) {
returnScores.push({
hash: block.id,
matchRate: block.extras.matchRate
});
} else {
let currentHash;
if (!currentHash && Common.indexingEnabled()) {
const dbBlock = await blocksRepository.$getBlockByHeight(currentHeight);
if (dbBlock && dbBlock['id']) {
currentHash = dbBlock['id'];
}
}
if (!currentHash) {
currentHash = await bitcoinApi.$getBlockHash(currentHeight);
}
if (currentHash) {
const auditScore = await blocksAuditsRepository.$getBlockAuditScore(currentHash);
returnScores.push({
hash: currentHash,
matchRate: auditScore?.matchRate
});
}
}
currentHeight--;
}
return returnScores;
}
} }
export default new Audit(); export default new Audit();

View File

@ -10,7 +10,7 @@ export interface AbstractBitcoinApi {
$getBlockHash(height: number): Promise<string>; $getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>; $getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>; $getBlock(hash: string): Promise<IEsploraApi.Block>;
$getRawBlock(hash: string): Promise<string>; $getRawBlock(hash: string): Promise<Buffer>;
$getAddress(address: string): Promise<IEsploraApi.Address>; $getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[]; $getAddressPrefix(prefix: string): string[];

View File

@ -81,7 +81,7 @@ class BitcoinApi implements AbstractBitcoinApi {
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
} }
$getRawBlock(hash: string): Promise<string> { $getRawBlock(hash: string): Promise<Buffer> {
return this.bitcoindClient.getBlock(hash, 0) return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex")); .then((raw: string) => Buffer.from(raw, "hex"));
} }

View File

@ -17,13 +17,14 @@ import logger from '../../logger';
import blocks from '../blocks'; import blocks from '../blocks';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment'; import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes) .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', this.getTransactionTimes)
.get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.getCpfpInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', this.$getCpfpInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange) .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', this.getDifficultyChange)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees) .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', this.getRecommendedFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', this.getMempoolBlocks)
@ -89,6 +90,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
; ;
@ -187,29 +189,36 @@ class BitcoinRoutes {
} }
} }
private getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
res.status(501).send(`Invalid transaction ID.`); res.status(501).send(`Invalid transaction ID.`);
return; return;
} }
const tx = mempool.getMempool()[req.params.txId]; const tx = mempool.getMempool()[req.params.txId];
if (!tx) { if (tx) {
res.status(404).send(`Transaction doesn't exist in the mempool.`); if (tx?.cpfpChecked) {
res.json({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant || null,
descendants: tx.descendants || null,
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
});
return;
}
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
res.json(cpfpInfo);
return; return;
} else {
const cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
if (cpfpInfo) {
res.json(cpfpInfo);
return;
}
} }
res.status(404).send(`Transaction has no CPFP info available.`);
if (tx.cpfpChecked) {
res.json({
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant || null,
});
return;
}
const cpfpInfo = Common.setRelativesAndGetCpfpInfo(tx, mempool.getMempool());
res.json(cpfpInfo);
} }
private getBackendInfo(req: Request, res: Response) { private getBackendInfo(req: Request, res: Response) {
@ -324,6 +333,16 @@ class BitcoinRoutes {
} }
} }
private async getStrippedBlockTransactions(req: Request, res: Response) {
try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) { private async getBlock(req: Request, res: Response) {
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@ -356,9 +375,9 @@ class BitcoinRoutes {
} }
} }
private async getStrippedBlockTransactions(req: Request, res: Response) { private async getBlockAuditSummary(req: Request, res: Response) {
try { try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); const transactions = await blocks.$getBlockAuditSummary(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {

View File

@ -55,9 +55,9 @@ class ElectrsApi implements AbstractBitcoinApi {
.then((response) => response.data); .then((response) => response.data);
} }
$getRawBlock(hash: string): Promise<string> { $getRawBlock(hash: string): Promise<Buffer> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig) return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
.then((response) => response.data); .then((response) => { return Buffer.from(response.data); });
} }
$getAddress(address: string): Promise<IEsploraApi.Address> { $getAddress(address: string): Promise<IEsploraApi.Address> {

View File

@ -21,10 +21,13 @@ import fiatConversion from './fiat-conversion';
import poolsParser from './pools-parser'; import poolsParser from './pools-parser';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import cpfpRepository from '../repositories/CpfpRepository';
import transactionRepository from '../repositories/TransactionRepository';
import mining from './mining/mining'; import mining from './mining/mining';
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
import PricesRepository from '../repositories/PricesRepository'; import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { Block } from 'bitcoinjs-lib';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@ -260,7 +263,7 @@ class Blocks {
/** /**
* [INDEXING] Index all blocks summaries for the block txs visualization * [INDEXING] Index all blocks summaries for the block txs visualization
*/ */
public async $generateBlocksSummariesDatabase() { public async $generateBlocksSummariesDatabase(): Promise<void> {
if (Common.blocksSummariesIndexingEnabled() === false) { if (Common.blocksSummariesIndexingEnabled() === false) {
return; return;
} }
@ -316,6 +319,57 @@ class Blocks {
} }
} }
/**
* [INDEXING] Index transaction CPFP data for all blocks
*/
public async $generateCPFPDatabase(): Promise<void> {
if (Common.cpfpIndexingEnabled() === false) {
return;
}
try {
// Get all indexed block hash
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
if (!unindexedBlocks?.length) {
return;
}
// Logging
let count = 0;
let countThisRun = 0;
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
for (const block of unindexedBlocks) {
// Logging
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
if (elapsedSeconds > 5) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
countThisRun = 0;
}
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
// Logging
count++;
countThisRun++;
}
if (count > 0) {
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
} else {
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
}
} catch (e) {
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
}
}
/** /**
* [INDEXING] Index all blocks metadata for the mining dashboard * [INDEXING] Index all blocks metadata for the mining dashboard
*/ */
@ -359,7 +413,7 @@ class Blocks {
} }
++indexedThisRun; ++indexedThisRun;
++totalIndexed; ++totalIndexed;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) { if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
@ -461,9 +515,13 @@ class Blocks {
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10); await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
await HashratesRepository.$deleteLastEntries(); await HashratesRepository.$deleteLastEntries();
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10); await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
await cpfpRepository.$deleteClustersFrom(lastBlock['height'] - 10);
for (let i = 10; i >= 0; --i) { for (let i = 10; i >= 0; --i) {
const newBlock = await this.$indexBlock(lastBlock['height'] - i); const newBlock = await this.$indexBlock(lastBlock['height'] - i);
await this.$getStrippedBlockTransactions(newBlock.id, true, true); await this.$getStrippedBlockTransactions(newBlock.id, true, true);
if (config.MEMPOOL.TRANSACTION_INDEXING) {
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
}
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment(); await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
@ -489,6 +547,9 @@ class Blocks {
if (Common.blocksSummariesIndexingEnabled() === true) { if (Common.blocksSummariesIndexingEnabled() === true) {
await this.$getStrippedBlockTransactions(blockExtended.id, true); await this.$getStrippedBlockTransactions(blockExtended.id, true);
} }
if (config.MEMPOOL.TRANSACTION_INDEXING) {
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
}
} }
} }
@ -590,7 +651,7 @@ class Blocks {
if (skipMemoryCache === false) { if (skipMemoryCache === false) {
// Check the memory cache // Check the memory cache
const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash); const cachedSummary = this.getBlockSummaries().find((b) => b.id === hash);
if (cachedSummary) { if (cachedSummary?.transactions?.length) {
return cachedSummary.transactions; return cachedSummary.transactions;
} }
} }
@ -598,7 +659,7 @@ class Blocks {
// Check if it's indexed in db // Check if it's indexed in db
if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) { if (skipDBLookup === false && Common.blocksSummariesIndexingEnabled() === true) {
const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash); const indexedSummary = await BlocksSummariesRepository.$getByBlockId(hash);
if (indexedSummary !== undefined) { if (indexedSummary !== undefined && indexedSummary?.transactions?.length) {
return indexedSummary.transactions; return indexedSummary.transactions;
} }
} }
@ -651,6 +712,22 @@ class Blocks {
return returnBlocks; return returnBlocks;
} }
public async $getBlockAuditSummary(hash: string): Promise<any> {
let summary;
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
summary = await BlocksAuditsRepository.$getBlockAudit(hash);
}
// fallback to non-audited transaction summary
if (!summary?.transactions?.length) {
const strippedTransactions = await this.$getStrippedBlockTransactions(hash);
summary = {
transactions: strippedTransactions
};
}
return summary;
}
public getLastDifficultyAdjustmentTime(): number { public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime; return this.lastDifficultyAdjustmentTime;
} }
@ -662,6 +739,62 @@ class Blocks {
public getCurrentBlockHeight(): number { public getCurrentBlockHeight(): number {
return this.currentBlockHeight; return this.currentBlockHeight;
} }
public async $indexCPFP(hash: string, height: number): Promise<void> {
let transactions;
if (false/*Common.blocksSummariesIndexingEnabled()*/) {
transactions = await this.$getStrippedBlockTransactions(hash);
const rawBlock = await bitcoinApi.$getRawBlock(hash);
const block = Block.fromBuffer(rawBlock);
const txMap = {};
for (const tx of block.transactions || []) {
txMap[tx.getId()] = tx;
}
for (const tx of transactions) {
if (txMap[tx.txid]?.ins) {
tx.vin = txMap[tx.txid].ins.map(vin => {
return {
txid: vin.hash
};
});
}
}
} else {
const block = await bitcoinClient.getBlock(hash, 2);
transactions = block.tx.map(tx => {
tx.vsize = tx.weight / 4;
return tx;
});
}
let cluster: TransactionStripped[] = [];
let ancestors: { [txid: string]: boolean } = {};
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
cluster.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += tx.vsize;
});
const effectiveFeePerVsize = (totalFee * 100_000_000) / totalVSize;
if (cluster.length > 1) {
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: (tx.fee || 0) * 100_000_000 }; }), effectiveFeePerVsize);
for (const tx of cluster) {
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
}
}
cluster = [];
ancestors = {};
}
cluster.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
await blocksRepository.$setCPFPIndexed(hash);
}
} }
export default new Blocks(); export default new Blocks();

View File

@ -187,6 +187,13 @@ export class Common {
); );
} }
static cpfpIndexingEnabled(): boolean {
return (
Common.indexingEnabled() &&
config.MEMPOOL.TRANSACTION_INDEXING === true
);
}
static setDateMidnight(date: Date): void { static setDateMidnight(date: Date): void {
date.setUTCHours(0); date.setUTCHours(0);
date.setUTCMinutes(0); date.setUTCMinutes(0);

View File

@ -4,8 +4,8 @@ import logger from '../logger';
import { Common } from './common'; import { Common } from './common';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 44; private static currentVersion = 49;
private queryTimeout = 900_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -107,18 +107,22 @@ class DatabaseMigration {
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics')); await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) { if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`); await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
await this.updateToSchemaVersion(2);
} }
if (databaseSchemaVersion < 3) { if (databaseSchemaVersion < 3) {
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools')); await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
await this.updateToSchemaVersion(3);
} }
if (databaseSchemaVersion < 4) { if (databaseSchemaVersion < 4) {
await this.$executeQuery('DROP table IF EXISTS blocks;'); await this.$executeQuery('DROP table IF EXISTS blocks;');
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks')); await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
await this.updateToSchemaVersion(4);
} }
if (databaseSchemaVersion < 5 && isBitcoin === true) { if (databaseSchemaVersion < 5 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage); this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(5);
} }
if (databaseSchemaVersion < 6 && isBitcoin === true) { if (databaseSchemaVersion < 6 && isBitcoin === true) {
@ -141,11 +145,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
await this.updateToSchemaVersion(6);
} }
if (databaseSchemaVersion < 7 && isBitcoin === true) { if (databaseSchemaVersion < 7 && isBitcoin === true) {
await this.$executeQuery('DROP table IF EXISTS hashrates;'); await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
await this.updateToSchemaVersion(7);
} }
if (databaseSchemaVersion < 8 && isBitcoin === true) { if (databaseSchemaVersion < 8 && isBitcoin === true) {
@ -155,6 +161,7 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
await this.updateToSchemaVersion(8);
} }
if (databaseSchemaVersion < 9 && isBitcoin === true) { if (databaseSchemaVersion < 9 && isBitcoin === true) {
@ -162,10 +169,12 @@ class DatabaseMigration {
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
await this.updateToSchemaVersion(9);
} }
if (databaseSchemaVersion < 10 && isBitcoin === true) { if (databaseSchemaVersion < 10 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)'); await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
await this.updateToSchemaVersion(10);
} }
if (databaseSchemaVersion < 11 && isBitcoin === true) { if (databaseSchemaVersion < 11 && isBitcoin === true) {
@ -178,11 +187,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(11);
} }
if (databaseSchemaVersion < 12 && isBitcoin === true) { if (databaseSchemaVersion < 12 && isBitcoin === true) {
// No need to re-index because the new data type can contain larger values // No need to re-index because the new data type can contain larger values
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(12);
} }
if (databaseSchemaVersion < 13 && isBitcoin === true) { if (databaseSchemaVersion < 13 && isBitcoin === true) {
@ -190,6 +201,7 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(13);
} }
if (databaseSchemaVersion < 14 && isBitcoin === true) { if (databaseSchemaVersion < 14 && isBitcoin === true) {
@ -197,37 +209,45 @@ class DatabaseMigration {
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`'); await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(14);
} }
if (databaseSchemaVersion < 16 && isBitcoin === true) { if (databaseSchemaVersion < 16 && isBitcoin === true) {
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage); this.uniqueLog(logger.notice, this.hashratesTruncatedMessage);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
await this.updateToSchemaVersion(16);
} }
if (databaseSchemaVersion < 17 && isBitcoin === true) { if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL'); await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
await this.updateToSchemaVersion(17);
} }
if (databaseSchemaVersion < 18 && isBitcoin === true) { if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);'); await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
await this.updateToSchemaVersion(18);
} }
if (databaseSchemaVersion < 19) { if (databaseSchemaVersion < 19) {
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates')); await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
await this.updateToSchemaVersion(19);
} }
if (databaseSchemaVersion < 20 && isBitcoin === true) { if (databaseSchemaVersion < 20 && isBitcoin === true) {
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries')); await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
await this.updateToSchemaVersion(20);
} }
if (databaseSchemaVersion < 21) { if (databaseSchemaVersion < 21) {
await this.$executeQuery('DROP TABLE IF EXISTS `rates`'); await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices')); await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
await this.updateToSchemaVersion(21);
} }
if (databaseSchemaVersion < 22 && isBitcoin === true) { if (databaseSchemaVersion < 22 && isBitcoin === true) {
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`'); await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments')); await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
await this.updateToSchemaVersion(22);
} }
if (databaseSchemaVersion < 23) { if (databaseSchemaVersion < 23) {
@ -240,11 +260,13 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
await this.updateToSchemaVersion(23);
} }
if (databaseSchemaVersion < 24 && isBitcoin == true) { if (databaseSchemaVersion < 24 && isBitcoin == true) {
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`'); await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits')); await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
await this.updateToSchemaVersion(24);
} }
if (databaseSchemaVersion < 25 && isBitcoin === true) { if (databaseSchemaVersion < 25 && isBitcoin === true) {
@ -252,6 +274,7 @@ class DatabaseMigration {
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
await this.updateToSchemaVersion(25);
} }
if (databaseSchemaVersion < 26 && isBitcoin === true) { if (databaseSchemaVersion < 26 && isBitcoin === true) {
@ -262,6 +285,7 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(26);
} }
if (databaseSchemaVersion < 27 && isBitcoin === true) { if (databaseSchemaVersion < 27 && isBitcoin === true) {
@ -271,6 +295,7 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(27);
} }
if (databaseSchemaVersion < 28 && isBitcoin === true) { if (databaseSchemaVersion < 28 && isBitcoin === true) {
@ -280,6 +305,7 @@ class DatabaseMigration {
await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`); await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
await this.updateToSchemaVersion(28);
} }
if (databaseSchemaVersion < 29 && isBitcoin === true) { if (databaseSchemaVersion < 29 && isBitcoin === true) {
@ -291,41 +317,50 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
await this.updateToSchemaVersion(29);
} }
if (databaseSchemaVersion < 30 && isBitcoin === true) { if (databaseSchemaVersion < 30 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL'); await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
await this.updateToSchemaVersion(30);
} }
if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices if (databaseSchemaVersion < 31 && isBitcoin == true) { // Link blocks to prices
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE'); await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`'); await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices')); await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
await this.updateToSchemaVersion(31);
} }
if (databaseSchemaVersion < 32 && isBitcoin == true) { if (databaseSchemaVersion < 32 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"'); await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
await this.updateToSchemaVersion(32);
} }
if (databaseSchemaVersion < 33 && isBitcoin == true) { if (databaseSchemaVersion < 33 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL'); await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
await this.updateToSchemaVersion(33);
} }
if (databaseSchemaVersion < 34 && isBitcoin == true) { if (databaseSchemaVersion < 34 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(34);
} }
if (databaseSchemaVersion < 35 && isBitcoin == true) { if (databaseSchemaVersion < 35 && isBitcoin == true) {
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"'); await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
await this.updateToSchemaVersion(35);
} }
if (databaseSchemaVersion < 36 && isBitcoin == true) { if (databaseSchemaVersion < 36 && isBitcoin == true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"'); await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
await this.updateToSchemaVersion(36);
} }
if (databaseSchemaVersion < 37 && isBitcoin == true) { if (databaseSchemaVersion < 37 && isBitcoin == true) {
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets')); await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
await this.updateToSchemaVersion(37);
} }
if (databaseSchemaVersion < 38 && isBitcoin == true) { if (databaseSchemaVersion < 38 && isBitcoin == true) {
@ -336,34 +371,76 @@ class DatabaseMigration {
await this.$executeQuery(`TRUNCATE node_stats`); await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL'); await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL'); await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
} }
if (databaseSchemaVersion < 39 && isBitcoin === true) { if (databaseSchemaVersion < 39 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`'); await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)'); await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
await this.updateToSchemaVersion(39);
} }
if (databaseSchemaVersion < 40 && isBitcoin === true) { if (databaseSchemaVersion < 40 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);'); await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
await this.updateToSchemaVersion(40);
} }
if (databaseSchemaVersion < 41 && isBitcoin === true) { if (databaseSchemaVersion < 41 && isBitcoin === true) {
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1'); await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
await this.updateToSchemaVersion(41);
} }
if (databaseSchemaVersion < 42 && isBitcoin === true) { if (databaseSchemaVersion < 42 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0'); await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
await this.updateToSchemaVersion(42);
} }
if (databaseSchemaVersion < 43 && isBitcoin === true) { if (databaseSchemaVersion < 43 && isBitcoin === true) {
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records')); await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
await this.updateToSchemaVersion(43);
} }
if (databaseSchemaVersion < 44 && isBitcoin === true) { if (databaseSchemaVersion < 44 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL'); await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
await this.updateToSchemaVersion(44);
}
if (databaseSchemaVersion < 45 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(45);
}
if (databaseSchemaVersion < 46) {
await this.$executeQuery(`ALTER TABLE blocks MODIFY blockTimestamp timestamp NOT NULL DEFAULT 0`);
await this.updateToSchemaVersion(46);
}
if (databaseSchemaVersion < 47) {
await this.$executeQuery('ALTER TABLE `blocks` ADD cpfp_indexed tinyint(1) DEFAULT 0');
await this.$executeQuery(this.getCreateCPFPTableQuery(), await this.$checkIfTableExists('cpfp_clusters'));
await this.$executeQuery(this.getCreateTransactionsTableQuery(), await this.$checkIfTableExists('transactions'));
await this.updateToSchemaVersion(47);
}
if (databaseSchemaVersion < 48 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(48);
}
if (databaseSchemaVersion < 49 && isBitcoin === true) {
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
await this.updateToSchemaVersion(49);
} }
} }
@ -502,6 +579,10 @@ class DatabaseMigration {
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`; return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
} }
private async updateToSchemaVersion(version): Promise<void> {
await this.$executeQuery(`UPDATE state SET number = ${version} WHERE name = 'schema_version';`);
}
/** /**
* Print current database version * Print current database version
*/ */
@ -813,6 +894,25 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
private getCreateCPFPTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS cpfp_clusters (
root varchar(65) NOT NULL,
height int(10) NOT NULL,
txs JSON DEFAULT NULL,
fee_rate double unsigned NOT NULL,
PRIMARY KEY (root)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateTransactionsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS transactions (
txid varchar(65) NOT NULL,
cluster varchar(65) DEFAULT NULL,
PRIMARY KEY (txid),
FOREIGN KEY (cluster) REFERENCES cpfp_clusters (root) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) { public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates', 'prices']; const allowedTables = ['blocks', 'hashrates', 'prices'];

View File

@ -128,6 +128,21 @@ class ChannelsApi {
} }
} }
public async $getChannelsWithoutSourceChecked(): Promise<any[]> {
try {
const query = `
SELECT channels.*
FROM channels
WHERE channels.source_checked != 1
`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('$getUnresolvedClosedChannels error: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getChannelsWithoutCreatedDate(): Promise<any[]> { public async $getChannelsWithoutCreatedDate(): Promise<any[]> {
try { try {
const query = `SELECT * FROM channels WHERE created IS NULL`; const query = `SELECT * FROM channels WHERE created IS NULL`;
@ -257,6 +272,108 @@ class ChannelsApi {
} }
} }
public async $getChannelByClosingId(transactionId: string): Promise<any> {
try {
const query = `
SELECT
channels.*
FROM channels
WHERE channels.closing_transaction_id = ?
`;
const [rows]: any = await DB.query(query, [transactionId]);
if (rows.length > 0) {
rows[0].outputs = JSON.parse(rows[0].outputs);
return rows[0];
}
} catch (e) {
logger.err('$getChannelByClosingId error: ' + (e instanceof Error ? e.message : e));
// don't throw - this data isn't essential
}
}
public async $getChannelsByOpeningId(transactionId: string): Promise<any> {
try {
const query = `
SELECT
channels.*
FROM channels
WHERE channels.transaction_id = ?
`;
const [rows]: any = await DB.query(query, [transactionId]);
if (rows.length > 0) {
return rows.map(row => {
row.outputs = JSON.parse(row.outputs);
return row;
});
}
} catch (e) {
logger.err('$getChannelsByOpeningId error: ' + (e instanceof Error ? e.message : e));
// don't throw - this data isn't essential
}
}
public async $updateClosingInfo(channelInfo: { id: string, node1_closing_balance: number, node2_closing_balance: number, closed_by: string | null, closing_fee: number, outputs: ILightningApi.ForensicOutput[]}): Promise<void> {
try {
const query = `
UPDATE channels SET
node1_closing_balance = ?,
node2_closing_balance = ?,
closed_by = ?,
closing_fee = ?,
outputs = ?
WHERE channels.id = ?
`;
await DB.query<ResultSetHeader>(query, [
channelInfo.node1_closing_balance || 0,
channelInfo.node2_closing_balance || 0,
channelInfo.closed_by,
channelInfo.closing_fee || 0,
JSON.stringify(channelInfo.outputs),
channelInfo.id,
]);
} catch (e) {
logger.err('$updateClosingInfo error: ' + (e instanceof Error ? e.message : e));
// don't throw - this data isn't essential
}
}
public async $updateOpeningInfo(channelInfo: { id: string, node1_funding_balance: number, node2_funding_balance: number, funding_ratio: number, single_funded: boolean | void }): Promise<void> {
try {
const query = `
UPDATE channels SET
node1_funding_balance = ?,
node2_funding_balance = ?,
funding_ratio = ?,
single_funded = ?
WHERE channels.id = ?
`;
await DB.query<ResultSetHeader>(query, [
channelInfo.node1_funding_balance || 0,
channelInfo.node2_funding_balance || 0,
channelInfo.funding_ratio,
channelInfo.single_funded ? 1 : 0,
channelInfo.id,
]);
} catch (e) {
logger.err('$updateOpeningInfo error: ' + (e instanceof Error ? e.message : e));
// don't throw - this data isn't essential
}
}
public async $markChannelSourceChecked(id: string): Promise<void> {
try {
const query = `
UPDATE channels
SET source_checked = 1
WHERE id = ?
`;
await DB.query<ResultSetHeader>(query, [id]);
} catch (e) {
logger.err('$markChannelSourceChecked error: ' + (e instanceof Error ? e.message : e));
// don't throw - this data isn't essential
}
}
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> { public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
try { try {
let channelStatusFilter; let channelStatusFilter;
@ -385,11 +502,15 @@ class ChannelsApi {
'transaction_id': channel.transaction_id, 'transaction_id': channel.transaction_id,
'transaction_vout': channel.transaction_vout, 'transaction_vout': channel.transaction_vout,
'closing_transaction_id': channel.closing_transaction_id, 'closing_transaction_id': channel.closing_transaction_id,
'closing_fee': channel.closing_fee,
'closing_reason': channel.closing_reason, 'closing_reason': channel.closing_reason,
'closing_date': channel.closing_date, 'closing_date': channel.closing_date,
'updated_at': channel.updated_at, 'updated_at': channel.updated_at,
'created': channel.created, 'created': channel.created,
'status': channel.status, 'status': channel.status,
'funding_ratio': channel.funding_ratio,
'closed_by': channel.closed_by,
'single_funded': !!channel.single_funded,
'node_left': { 'node_left': {
'alias': channel.alias_left, 'alias': channel.alias_left,
'public_key': channel.node1_public_key, 'public_key': channel.node1_public_key,
@ -404,6 +525,9 @@ class ChannelsApi {
'updated_at': channel.node1_updated_at, 'updated_at': channel.node1_updated_at,
'longitude': channel.node1_longitude, 'longitude': channel.node1_longitude,
'latitude': channel.node1_latitude, 'latitude': channel.node1_latitude,
'funding_balance': channel.node1_funding_balance,
'closing_balance': channel.node1_closing_balance,
'initiated_close': channel.closed_by === channel.node1_public_key ? true : undefined,
}, },
'node_right': { 'node_right': {
'alias': channel.alias_right, 'alias': channel.alias_right,
@ -419,6 +543,9 @@ class ChannelsApi {
'updated_at': channel.node2_updated_at, 'updated_at': channel.node2_updated_at,
'longitude': channel.node2_longitude, 'longitude': channel.node2_longitude,
'latitude': channel.node2_latitude, 'latitude': channel.node2_latitude,
'funding_balance': channel.node2_funding_balance,
'closing_balance': channel.node2_closing_balance,
'initiated_close': channel.closed_by === channel.node2_public_key ? true : undefined,
}, },
}; };
} }

View File

@ -83,4 +83,10 @@ export namespace ILightningApi {
is_required: boolean; is_required: boolean;
is_known: boolean; is_known: boolean;
} }
export interface ForensicOutput {
node?: 1 | 2;
type: number;
value: number;
}
} }

View File

@ -155,6 +155,7 @@ class MempoolBlocks {
if (newMempool[txid] && mempool[txid]) { if (newMempool[txid] && mempool[txid]) {
newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize; newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize;
newMempool[txid].ancestors = mempool[txid].ancestors; newMempool[txid].ancestors = mempool[txid].ancestors;
newMempool[txid].descendants = mempool[txid].descendants;
newMempool[txid].bestDescendant = mempool[txid].bestDescendant; newMempool[txid].bestDescendant = mempool[txid].bestDescendant;
newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked; newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked;
} }

View File

@ -283,9 +283,12 @@ class MiningRoutes {
private async $getBlockAuditScores(req: Request, res: Response) { private async $getBlockAuditScores(req: Request, res: Response) {
try { try {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); let height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
if (height == null) {
height = await BlocksRepository.$mostRecentBlockHeight();
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await audits.$getBlockAuditScores(height, 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

View File

@ -108,36 +108,38 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }:
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) { if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
blockWeight += nextTx.ancestorWeight; blockWeight += nextTx.ancestorWeight;
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values()); const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
const descendants: AuditTransaction[] = [];
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count) // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
sortedTxSet.forEach((ancestor, i, arr) => {
while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop();
const mempoolTx = mempool[ancestor.txid]; const mempoolTx = mempool[ancestor.txid];
if (ancestor && !ancestor?.used) { if (ancestor && !ancestor?.used) {
ancestor.used = true; ancestor.used = true;
// update original copy of this tx with effective fee rate & relatives data // update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = effectiveFeeRate; mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => { mempoolTx.ancestors = sortedTxSet.map((a) => {
return {
txid: a.txid,
fee: a.fee,
weight: a.weight,
};
}).reverse();
mempoolTx.descendants = descendants.map((a) => {
return { return {
txid: a.txid, txid: a.txid,
fee: a.fee, fee: a.fee,
weight: a.weight, weight: a.weight,
}; };
}); });
descendants.push(ancestor);
mempoolTx.cpfpChecked = true; mempoolTx.cpfpChecked = true;
if (i < arr.length - 1) {
mempoolTx.bestDescendant = {
txid: arr[arr.length - 1].txid,
fee: arr[arr.length - 1].fee,
weight: arr[arr.length - 1].weight,
};
} else {
mempoolTx.bestDescendant = null;
}
transactions.push(ancestor); transactions.push(ancestor);
blockSize += ancestor.size; blockSize += ancestor.size;
} }
}); }
// remove these as valid package ancestors for any descendants remaining in the mempool // remove these as valid package ancestors for any descendants remaining in the mempool
if (sortedTxSet.length) { if (sortedTxSet.length) {

View File

@ -250,12 +250,12 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true); await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true);
} } else {
else {
mempoolBlocks.updateMempoolBlocks(newMempool); mempoolBlocks.updateMempoolBlocks(newMempool);
} }
const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo(); const mempoolInfo = memPool.getMempoolInfo();
@ -417,9 +417,8 @@ class WebsocketHandler {
} }
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
let matchRate;
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
await mempoolBlocks.makeBlockTemplates(_memPool, 2); await mempoolBlocks.makeBlockTemplates(_memPool, 2);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool); mempoolBlocks.updateMempoolBlocks(_memPool);
@ -428,8 +427,8 @@ class WebsocketHandler {
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool); const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
return { return {
@ -454,6 +453,7 @@ class WebsocketHandler {
hash: block.id, hash: block.id,
addedTxs: added, addedTxs: added,
missingTxs: censored, missingTxs: censored,
freshTxs: fresh,
matchRate: matchRate, matchRate: matchRate,
}); });
@ -467,7 +467,7 @@ class WebsocketHandler {
delete _memPool[txId]; delete _memPool[txId];
} }
if (config.MEMPOOL.ADVANCED_TRANSACTION_SELECTION) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.makeBlockTemplates(_memPool, 2); await mempoolBlocks.makeBlockTemplates(_memPool, 2);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool); mempoolBlocks.updateMempoolBlocks(_memPool);

View File

@ -29,7 +29,9 @@ interface IConfig {
AUTOMATIC_BLOCK_REINDEXING: boolean; AUTOMATIC_BLOCK_REINDEXING: boolean;
POOLS_JSON_URL: string, POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string, POOLS_JSON_TREE_URL: string,
ADVANCED_TRANSACTION_SELECTION: boolean; ADVANCED_GBT_AUDIT: boolean;
ADVANCED_GBT_MEMPOOL: boolean;
TRANSACTION_INDEXING: boolean;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@ -147,7 +149,9 @@ const defaults: IConfig = {
'AUTOMATIC_BLOCK_REINDEXING': false, 'AUTOMATIC_BLOCK_REINDEXING': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'ADVANCED_TRANSACTION_SELECTION': false, 'ADVANCED_GBT_AUDIT': false,
'ADVANCED_GBT_MEMPOOL': false,
'TRANSACTION_INDEXING': false,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',

View File

@ -77,6 +77,7 @@ class Indexer {
await mining.$generateNetworkHashrateHistory(); await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory(); await mining.$generatePoolHashrateHistory();
await blocks.$generateBlocksSummariesDatabase(); await blocks.$generateBlocksSummariesDatabase();
await blocks.$generateCPFPDatabase();
} catch (e) { } catch (e) {
this.indexerRunning = false; this.indexerRunning = false;
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -28,6 +28,7 @@ export interface BlockAudit {
height: number, height: number,
hash: string, hash: string,
missingTxs: string[], missingTxs: string[],
freshTxs: string[],
addedTxs: string[], addedTxs: string[],
matchRate: number, matchRate: number,
} }
@ -71,6 +72,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
firstSeen?: number; firstSeen?: number;
effectiveFeePerVsize: number; effectiveFeePerVsize: number;
ancestors?: Ancestor[]; ancestors?: Ancestor[];
descendants?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
deleteAfter?: number; deleteAfter?: number;
@ -118,7 +120,9 @@ interface BestDescendant {
export interface CpfpInfo { export interface CpfpInfo {
ancestors: Ancestor[]; ancestors: Ancestor[];
bestDescendant: BestDescendant | null; bestDescendant?: BestDescendant | null;
descendants?: Ancestor[];
effectiveFeePerVsize?: number;
} }
export interface TransactionStripped { export interface TransactionStripped {

View File

@ -1,3 +1,4 @@
import blocks from '../api/blocks';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces'; import { BlockAudit, AuditScore } from '../mempool.interfaces';
@ -5,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { public async $saveAudit(audit: BlockAudit): Promise<void> {
try { try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate) await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, match_rate)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs), VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), audit.matchRate]); JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), audit.matchRate]);
} 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 block audit for block ${audit.hash} because it has already been indexed, ignoring`); logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@ -51,7 +52,7 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size,
blocks.weight, blocks.tx_count, blocks.weight, blocks.tx_count,
transactions, template, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate transactions, template, missing_txs as missingTxs, added_txs as addedTxs, fresh_txs as freshTxs, match_rate as matchRate
FROM blocks_audits FROM blocks_audits
JOIN blocks ON blocks.hash = blocks_audits.hash JOIN blocks ON blocks.hash = blocks_audits.hash
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
@ -61,11 +62,15 @@ class BlocksAuditRepositories {
if (rows.length) { if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
rows[0].addedTxs = JSON.parse(rows[0].addedTxs); rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].transactions = JSON.parse(rows[0].transactions); rows[0].transactions = JSON.parse(rows[0].transactions);
rows[0].template = JSON.parse(rows[0].template); rows[0].template = JSON.parse(rows[0].template);
if (rows[0].transactions.length) {
return rows[0];
}
} }
return null;
return rows[0];
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
@ -85,6 +90,20 @@ class BlocksAuditRepositories {
throw e; throw e;
} }
} }
public async $getBlockAuditScores(maxHeight: number, minHeight: number): Promise<AuditScore[]> {
try {
const [rows]: any[] = await DB.query(
`SELECT hash, match_rate as matchRate
FROM blocks_audits
WHERE blocks_audits.height BETWEEN ? AND ?
`, [minHeight, maxHeight]);
return rows;
} catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
} }
export default new BlocksAuditRepositories(); export default new BlocksAuditRepositories();

View File

@ -662,6 +662,23 @@ class BlocksRepository {
} }
} }
/**
* Get a list of blocks that have not had CPFP data indexed
*/
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
try {
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
return rows;
} catch (e) {
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $setCPFPIndexed(hash: string): Promise<void> {
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
}
/** /**
* Return the oldest block from a consecutive chain of block from the most recent one * Return the oldest block from a consecutive chain of block from the most recent one
*/ */

View File

@ -0,0 +1,43 @@
import DB from '../database';
import logger from '../logger';
import { Ancestor } from '../mempool.interfaces';
class CpfpRepository {
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
try {
const txsJson = JSON.stringify(txs);
await DB.query(
`
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
VALUE (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
height = ?,
txs = ?,
fee_rate = ?
`,
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
);
} catch (e: any) {
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $deleteClustersFrom(height: number): Promise<void> {
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
try {
await DB.query(
`
DELETE from cpfp_clusters
WHERE height >= ?
`,
[height]
);
} catch (e: any) {
logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new CpfpRepository();

View File

@ -0,0 +1,77 @@
import DB from '../database';
import logger from '../logger';
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
interface CpfpSummary {
txid: string;
cluster: string;
root: string;
txs: Ancestor[];
height: number;
fee_rate: number;
}
class TransactionRepository {
public async $setCluster(txid: string, cluster: string): Promise<void> {
try {
await DB.query(
`
INSERT INTO transactions
(
txid,
cluster
)
VALUE (?, ?)
ON DUPLICATE KEY UPDATE
cluster = ?
;`,
[txid, cluster, cluster]
);
} catch (e: any) {
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
try {
let query = `
SELECT *
FROM transactions
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
WHERE transactions.txid = ?
`;
const [rows]: any = await DB.query(query, [txid]);
if (rows.length) {
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
return this.convertCpfp(rows[0]);
}
} catch (e) {
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
const descendants: Ancestor[] = [];
const ancestors: Ancestor[] = [];
let matched = false;
for (const tx of cpfp.txs) {
if (tx.txid === cpfp.txid) {
matched = true;
} else if (!matched) {
descendants.push(tx);
} else {
ancestors.push(tx);
}
}
return {
descendants,
ancestors,
effectiveFeePerVsize: cpfp.fee_rate
};
}
}
export default new TransactionRepository();

View File

@ -5,13 +5,16 @@ import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
import config from '../../config'; import config from '../../config';
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
import { Common } from '../../api/common'; import { Common } from '../../api/common';
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
const throttleDelay = 20; //ms const throttleDelay = 20; //ms
const tempCacheSize = 10000;
class ForensicsService { class ForensicsService {
loggerTimer = 0; loggerTimer = 0;
closedChannelsScanBlock = 0; closedChannelsScanBlock = 0;
txCache: { [txid: string]: IEsploraApi.Transaction } = {}; txCache: { [txid: string]: IEsploraApi.Transaction } = {};
tempCached: string[] = [];
constructor() {} constructor() {}
@ -29,6 +32,7 @@ class ForensicsService {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
await this.$runClosedChannelsForensics(false); await this.$runClosedChannelsForensics(false);
await this.$runOpenedChannelsForensics();
} }
} catch (e) { } catch (e) {
@ -95,16 +99,9 @@ class ForensicsService {
const lightningScriptReasons: number[] = []; const lightningScriptReasons: number[] = [];
for (const outspend of outspends) { for (const outspend of outspends) {
if (outspend.spent && outspend.txid) { if (outspend.spent && outspend.txid) {
let spendingTx: IEsploraApi.Transaction | undefined = this.txCache[outspend.txid]; let spendingTx = await this.fetchTransaction(outspend.txid);
if (!spendingTx) { if (!spendingTx) {
try { continue;
spendingTx = await bitcoinApi.$getRawTransaction(outspend.txid);
await Common.sleep$(throttleDelay);
this.txCache[outspend.txid] = spendingTx;
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
} }
cached.push(spendingTx.txid); cached.push(spendingTx.txid);
const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]); const lightningScript = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
@ -124,16 +121,9 @@ class ForensicsService {
We can detect a commitment transaction (force close) by reading Sequence and Locktime We can detect a commitment transaction (force close) by reading Sequence and Locktime
https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction
*/ */
let closingTx: IEsploraApi.Transaction | undefined = this.txCache[channel.closing_transaction_id]; let closingTx = await this.fetchTransaction(channel.closing_transaction_id, true);
if (!closingTx) { if (!closingTx) {
try { continue;
closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id);
await Common.sleep$(throttleDelay);
this.txCache[channel.closing_transaction_id] = closingTx;
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + channel.closing_transaction_id}. Reason ${e instanceof Error ? e.message : e}`);
continue;
}
} }
cached.push(closingTx.txid); cached.push(closingTx.txid);
const sequenceHex: string = closingTx.vin[0].sequence.toString(16); const sequenceHex: string = closingTx.vin[0].sequence.toString(16);
@ -174,7 +164,7 @@ class ForensicsService {
} }
private findLightningScript(vin: IEsploraApi.Vin): number { private findLightningScript(vin: IEsploraApi.Vin): number {
const topElement = vin.witness[vin.witness.length - 2]; const topElement = vin.witness?.length > 2 ? vin.witness[vin.witness.length - 2] : null;
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) { if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs // https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
if (topElement === '01') { if (topElement === '01') {
@ -193,7 +183,7 @@ class ForensicsService {
) { ) {
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs // https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs // https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
if (topElement.length === 66) { if (topElement?.length === 66) {
// top element is a public key // top element is a public key
// 'Revoked Lightning HTLC'; Penalty force closed // 'Revoked Lightning HTLC'; Penalty force closed
return 4; return 4;
@ -220,6 +210,249 @@ class ForensicsService {
} }
return 1; return 1;
} }
// If a channel open tx spends funds from a another channel transaction,
// we can attribute that output to a specific counterparty
private async $runOpenedChannelsForensics(): Promise<void> {
const runTimer = Date.now();
let progress = 0;
try {
logger.info(`Started running open channel forensics...`);
const channels = await channelsApi.$getChannelsWithoutSourceChecked();
for (const openChannel of channels) {
let openTx = await this.fetchTransaction(openChannel.transaction_id, true);
if (!openTx) {
continue;
}
for (const input of openTx.vin) {
const closeChannel = await channelsApi.$getChannelByClosingId(input.txid);
if (closeChannel) {
// this input directly spends a channel close output
await this.$attributeChannelBalances(closeChannel, openChannel, input);
} else {
const prevOpenChannels = await channelsApi.$getChannelsByOpeningId(input.txid);
if (prevOpenChannels?.length) {
// this input spends a channel open change output
for (const prevOpenChannel of prevOpenChannels) {
await this.$attributeChannelBalances(prevOpenChannel, openChannel, input, null, null, true);
}
} else {
// check if this input spends any swept channel close outputs
await this.$attributeSweptChannelCloses(openChannel, input);
}
}
}
// calculate how much of the total input value is attributable to the channel open output
openChannel.funding_ratio = openTx.vout[openChannel.transaction_vout].value / ((openTx.vout.reduce((sum, v) => sum + v.value, 0) || 1) + openTx.fee);
// save changes to the opening channel, and mark it as checked
if (openTx?.vin?.length === 1) {
openChannel.single_funded = true;
}
if (openChannel.node1_funding_balance || openChannel.node2_funding_balance || openChannel.node1_closing_balance || openChannel.node2_closing_balance || openChannel.closed_by) {
await channelsApi.$updateOpeningInfo(openChannel);
}
await channelsApi.$markChannelSourceChecked(openChannel.id);
++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) {
logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
this.loggerTimer = new Date().getTime() / 1000;
this.truncateTempCache();
}
if (Date.now() - runTimer > (config.LIGHTNING.FORENSICS_INTERVAL * 1000)) {
break;
}
}
logger.info(`Open channels forensics scan complete.`);
} catch (e) {
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
} finally {
this.clearTempCache();
}
}
// Check if a channel open tx input spends the result of a swept channel close output
private async $attributeSweptChannelCloses(openChannel: ILightningApi.Channel, input: IEsploraApi.Vin): Promise<void> {
let sweepTx = await this.fetchTransaction(input.txid, true);
if (!sweepTx) {
logger.err(`couldn't find input transaction for channel forensics ${openChannel.channel_id} ${input.txid}`);
return;
}
const openContribution = sweepTx.vout[input.vout].value;
for (const sweepInput of sweepTx.vin) {
const lnScriptType = this.findLightningScript(sweepInput);
if (lnScriptType > 1) {
const closeChannel = await channelsApi.$getChannelByClosingId(sweepInput.txid);
if (closeChannel) {
const initiator = (lnScriptType === 2 || lnScriptType === 4) ? 'remote' : (lnScriptType === 3 ? 'local' : null);
await this.$attributeChannelBalances(closeChannel, openChannel, sweepInput, openContribution, initiator);
}
}
}
}
private async $attributeChannelBalances(
prevChannel, openChannel, input: IEsploraApi.Vin, openContribution: number | null = null,
initiator: 'remote' | 'local' | null = null, linkedOpenings: boolean = false
): Promise<void> {
// figure out which node controls the input/output
let openSide;
let prevLocal;
let prevRemote;
let matched = false;
let ambiguous = false; // if counterparties are the same in both channels, we can't tell them apart
if (openChannel.node1_public_key === prevChannel.node1_public_key) {
openSide = 1;
prevLocal = 1;
prevRemote = 2;
matched = true;
} else if (openChannel.node1_public_key === prevChannel.node2_public_key) {
openSide = 1;
prevLocal = 2;
prevRemote = 1;
matched = true;
}
if (openChannel.node2_public_key === prevChannel.node1_public_key) {
openSide = 2;
prevLocal = 1;
prevRemote = 2;
if (matched) {
ambiguous = true;
}
matched = true;
} else if (openChannel.node2_public_key === prevChannel.node2_public_key) {
openSide = 2;
prevLocal = 2;
prevRemote = 1;
if (matched) {
ambiguous = true;
}
matched = true;
}
if (matched && !ambiguous) {
// fetch closing channel transaction and perform forensics on the outputs
let prevChannelTx = await this.fetchTransaction(input.txid, true);
let outspends: IEsploraApi.Outspend[] | undefined;
try {
outspends = await bitcoinApi.$getOutspends(input.txid);
await Common.sleep$(throttleDelay);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + input.txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
}
if (!outspends || !prevChannelTx) {
return;
}
if (!linkedOpenings) {
if (!prevChannel.outputs || !prevChannel.outputs.length) {
prevChannel.outputs = prevChannelTx.vout.map(vout => {
return {
type: 0,
value: vout.value,
};
});
}
for (let i = 0; i < outspends?.length; i++) {
const outspend = outspends[i];
const output = prevChannel.outputs[i];
if (outspend.spent && outspend.txid) {
try {
const spendingTx = await this.fetchTransaction(outspend.txid, true);
if (spendingTx) {
output.type = this.findLightningScript(spendingTx.vin[outspend.vin || 0]);
}
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + outspend.txid}. Reason ${e instanceof Error ? e.message : e}`);
}
} else {
output.type = 0;
}
}
// attribute outputs to each counterparty, and sum up total known balances
prevChannel.outputs[input.vout].node = prevLocal;
const isPenalty = prevChannel.outputs.filter((out) => out.type === 2 || out.type === 4)?.length > 0;
const normalOutput = [1,3].includes(prevChannel.outputs[input.vout].type);
const mutualClose = ((prevChannel.status === 2 || prevChannel.status === 'closed') && prevChannel.closing_reason === 1);
let localClosingBalance = 0;
let remoteClosingBalance = 0;
for (const output of prevChannel.outputs) {
if (isPenalty) {
// penalty close, so local node takes everything
localClosingBalance += output.value;
} else if (output.node) {
// this output determinstically linked to one of the counterparties
if (output.node === prevLocal) {
localClosingBalance += output.value;
} else {
remoteClosingBalance += output.value;
}
} else if (normalOutput && (output.type === 1 || output.type === 3 || (mutualClose && prevChannel.outputs.length === 2))) {
// local node had one main output, therefore remote node takes the other
remoteClosingBalance += output.value;
}
}
prevChannel[`node${prevLocal}_closing_balance`] = localClosingBalance;
prevChannel[`node${prevRemote}_closing_balance`] = remoteClosingBalance;
prevChannel.closing_fee = prevChannelTx.fee;
if (initiator && !linkedOpenings) {
const initiatorSide = initiator === 'remote' ? prevRemote : prevLocal;
prevChannel.closed_by = prevChannel[`node${initiatorSide}_public_key`];
}
// save changes to the closing channel
await channelsApi.$updateClosingInfo(prevChannel);
} else {
if (prevChannelTx.vin.length <= 1) {
prevChannel[`node${prevLocal}_funding_balance`] = prevChannel.capacity;
prevChannel.single_funded = true;
prevChannel.funding_ratio = 1;
// save changes to the closing channel
await channelsApi.$updateOpeningInfo(prevChannel);
}
}
openChannel[`node${openSide}_funding_balance`] = openChannel[`node${openSide}_funding_balance`] + (openContribution || prevChannelTx?.vout[input.vout]?.value || 0);
}
}
async fetchTransaction(txid: string, temp: boolean = false): Promise<IEsploraApi.Transaction | null> {
let tx = this.txCache[txid];
if (!tx) {
try {
tx = await bitcoinApi.$getRawTransaction(txid);
this.txCache[txid] = tx;
if (temp) {
this.tempCached.push(txid);
}
await Common.sleep$(throttleDelay);
} catch (e) {
logger.err(`Failed to call ${config.ESPLORA.REST_API_URL + '/tx/' + txid + '/outspends'}. Reason ${e instanceof Error ? e.message : e}`);
return null;
}
}
return tx;
}
clearTempCache(): void {
for (const txid of this.tempCached) {
delete this.txCache[txid];
}
this.tempCached = [];
}
truncateTempCache(): void {
if (this.tempCached.length > tempCacheSize) {
const removed = this.tempCached.splice(0, this.tempCached.length - tempCacheSize);
for (const txid of removed) {
delete this.txCache[txid];
}
}
}
} }
export default new ForensicsService(); export default new ForensicsService();

View File

@ -31,6 +31,7 @@ class NetworkSyncService {
} }
private async $runTasks(): Promise<void> { private async $runTasks(): Promise<void> {
const taskStartTime = Date.now();
try { try {
logger.info(`Updating nodes and channels`); logger.info(`Updating nodes and channels`);
@ -57,7 +58,7 @@ class NetworkSyncService {
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e)); logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
} }
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL); setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime)));
} }
/** /**

View File

@ -268,57 +268,6 @@
} }
} }
}, },
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json",
"sourceMap": true,
"optimization": false
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"localize": true,
"optimization": true
}
},
"defaultConfiguration": ""
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"options": {
"browserTarget": "mempool:build",
"serverTarget": "mempool:server"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production"
}
}
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"routes": [
"/"
]
},
"configurations": {
"production": {}
}
},
"cypress-run": { "cypress-run": {
"builder": "@cypress/schematic:cypress", "builder": "@cypress/schematic:cypress",
"options": { "options": {
@ -339,6 +288,5 @@
} }
} }
} }
}, }
"defaultProject": "mempool"
} }

14552
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -51,9 +51,6 @@
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config", "config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config", "config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config", "config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"dev:ssr": "npm run generate-config && npm run ng -- run mempool:serve-ssr",
"serve:ssr": "node server.run.js",
"build:ssr": "npm run build && npm run ng -- run mempool:server:production && npm run tsc -- server.run.ts",
"prerender": "npm run ng -- run mempool:prerender", "prerender": "npm run ng -- run mempool:prerender",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run", "cypress:run": "cypress run",
@ -64,63 +61,59 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
}, },
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "~13.3.7", "@angular-devkit/build-angular": "^14.2.10",
"@angular/animations": "~13.3.10", "@angular/animations": "^14.2.12",
"@angular/cli": "~13.3.7", "@angular/cli": "^14.2.10",
"@angular/common": "~13.3.10", "@angular/common": "^14.2.12",
"@angular/compiler": "~13.3.10", "@angular/compiler": "^14.2.12",
"@angular/core": "~13.3.10", "@angular/core": "^14.2.12",
"@angular/forms": "~13.3.10", "@angular/forms": "^14.2.12",
"@angular/localize": "~13.3.10", "@angular/localize": "^14.2.12",
"@angular/platform-browser": "~13.3.10", "@angular/platform-browser": "^14.2.12",
"@angular/platform-browser-dynamic": "~13.3.10", "@angular/platform-browser-dynamic": "^14.2.12",
"@angular/platform-server": "~13.3.10", "@angular/platform-server": "^14.2.12",
"@angular/router": "~13.3.10", "@angular/router": "^14.2.12",
"@fortawesome/angular-fontawesome": "~0.10.2", "@fortawesome/angular-fontawesome": "~0.11.1",
"@fortawesome/fontawesome-common-types": "~6.1.1", "@fortawesome/fontawesome-common-types": "~6.2.1",
"@fortawesome/fontawesome-svg-core": "~6.1.1", "@fortawesome/fontawesome-svg-core": "~6.2.1",
"@fortawesome/free-solid-svg-icons": "~6.1.1", "@fortawesome/free-solid-svg-icons": "~6.2.1",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^13.1.1",
"@nguniversal/express-engine": "~13.1.1", "@types/qrcode": "~1.5.0",
"@types/qrcode": "~1.4.2", "bootstrap": "~4.6.1",
"bootstrap": "~4.5.0",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"clipboard": "^2.0.10", "clipboard": "^2.0.11",
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.3.2", "echarts": "~5.4.0",
"echarts-gl": "^2.0.9", "echarts-gl": "^2.0.9",
"express": "^4.17.1",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-echarts": "8.0.1", "ngx-echarts": "~14.0.0",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^14.0.1",
"qrcode": "1.5.0", "qrcode": "1.5.1",
"rxjs": "~7.5.5", "rxjs": "~7.5.7",
"tinyify": "^3.0.0", "tinyify": "^3.1.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.4.0", "tslib": "~2.4.1",
"zone.js": "~0.11.5" "zone.js": "~0.12.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "~13.3.10", "@angular/compiler-cli": "^14.2.12",
"@angular/language-service": "~13.3.10", "@angular/language-service": "^14.2.12",
"@nguniversal/builders": "~13.1.1", "@types/node": "^18.11.9",
"@types/express": "^4.17.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@types/node": "^12.11.1", "@typescript-eslint/parser": "^5.45.0",
"@typescript-eslint/eslint-plugin": "^5.30.5", "eslint": "^8.28.0",
"@typescript-eslint/parser": "^5.30.5",
"eslint": "^8.19.0",
"http-proxy-middleware": "~2.0.6", "http-proxy-middleware": "~2.0.6",
"prettier": "^2.7.1", "prettier": "^2.8.0",
"ts-node": "~10.8.1", "ts-node": "~10.9.1",
"typescript": "~4.6.4" "typescript": "~4.6.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "~2.0.0", "@cypress/schematic": "~2.3.0",
"cypress": "^10.3.0", "cypress": "^11.2.0",
"cypress-fail-on-console-error": "~3.0.0", "cypress-fail-on-console-error": "~4.0.2",
"cypress-wait-until": "^1.7.2", "cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.4", "mock-socket": "~9.1.5",
"start-server-and-test": "~1.14.0" "start-server-and-test": "~1.14.0"
}, },
"scarfSettings": { "scarfSettings": {

View File

@ -3,9 +3,9 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); entry.target = entry.target.replace("mempool.space", "mempool-staging.tk7.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); entry.target = entry.target.replace("liquid.network", "liquid-staging.tk7.mempool.space");
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space"); entry.target = entry.target.replace("bisq.markets", "bisq-staging.tk7.mempool.space");
}); });
module.exports = PROXY_CONFIG; module.exports = PROXY_CONFIG;

View File

@ -1,96 +0,0 @@
import 'zone.js/node';
import './generated-config';
import * as domino from 'domino';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
const {readFileSync, existsSync} = require('fs');
const {createProxyMiddleware} = require('http-proxy-middleware');
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
/**
* Return the list of supported and actually active locales
*/
function getActiveLocales() {
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
const supportedLocales = [
angularConfig.projects.mempool.i18n.sourceLocale,
...Object.keys(angularConfig.projects.mempool.i18n.locales),
];
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
}
function app() {
const server = express();
// proxy API to nginx
server.get('/api/**', createProxyMiddleware({
// @ts-ignore
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
changeOrigin: true,
}));
// map / and /en to en-US
const defaultLocale = 'en-US';
console.log(`serving default locale: ${defaultLocale}`);
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
server.use('/', appServerModule.app(defaultLocale));
server.use('/en', appServerModule.app(defaultLocale));
// map each locale to its localized main.js
getActiveLocales().forEach(locale => {
console.log('serving locale:', locale);
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
// map everything to itself
server.use(`/${locale}`, appServerModule.app(locale));
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
app().listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
run();

View File

@ -1,160 +0,0 @@
import 'zone.js/node';
import './generated-config';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as domino from 'domino';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
// The Express app is exported so that it can be used by serverless Functions.
export function app(locale: string): express.Express {
const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// only handle URLs that actually exist
//server.get(locale, getLocalizedSSR(indexHtml));
server.get('/', getLocalizedSSR(indexHtml));
server.get('/tx/*', getLocalizedSSR(indexHtml));
server.get('/block/*', getLocalizedSSR(indexHtml));
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/address/*', getLocalizedSSR(indexHtml));
server.get('/blocks', getLocalizedSSR(indexHtml));
server.get('/mining/pools', getLocalizedSSR(indexHtml));
server.get('/mining/pool/*', getLocalizedSSR(indexHtml));
server.get('/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid', getLocalizedSSR(indexHtml));
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
server.get('/liquid/api', getLocalizedSSR(indexHtml));
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
server.get('/liquid/status', getLocalizedSSR(indexHtml));
server.get('/liquid/about', getLocalizedSSR(indexHtml));
server.get('/testnet', getLocalizedSSR(indexHtml));
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
server.get('/testnet/mining/pools', getLocalizedSSR(indexHtml));
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
server.get('/testnet/api', getLocalizedSSR(indexHtml));
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
server.get('/testnet/status', getLocalizedSSR(indexHtml));
server.get('/testnet/about', getLocalizedSSR(indexHtml));
server.get('/signet', getLocalizedSSR(indexHtml));
server.get('/signet/tx/*', getLocalizedSSR(indexHtml));
server.get('/signet/block/*', getLocalizedSSR(indexHtml));
server.get('/signet/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/signet/address/*', getLocalizedSSR(indexHtml));
server.get('/signet/blocks', getLocalizedSSR(indexHtml));
server.get('/signet/mining/pools', getLocalizedSSR(indexHtml));
server.get('/signet/graphs', getLocalizedSSR(indexHtml));
server.get('/signet/api', getLocalizedSSR(indexHtml));
server.get('/signet/tv', getLocalizedSSR(indexHtml));
server.get('/signet/status', getLocalizedSSR(indexHtml));
server.get('/signet/about', getLocalizedSSR(indexHtml));
server.get('/bisq', getLocalizedSSR(indexHtml));
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
server.get('/bisq/about', getLocalizedSSR(indexHtml));
server.get('/bisq/api', getLocalizedSSR(indexHtml));
server.get('/about', getLocalizedSSR(indexHtml));
server.get('/api', getLocalizedSSR(indexHtml));
server.get('/tv', getLocalizedSSR(indexHtml));
server.get('/status', getLocalizedSSR(indexHtml));
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
// fallback to static file handler so we send HTTP 404 to nginx
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
return server;
}
function getLocalizedSSR(indexHtml) {
return (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
}
}
// only used for development mode
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app('en-US');
server.listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

View File

@ -4,7 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component'; import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component'; import { BlockComponent } from './components/block/block.component';
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
import { AddressComponent } from './components/address/address.component'; import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
@ -103,16 +102,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent,
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule), loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
@ -219,16 +208,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent,
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
@ -331,16 +310,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'block-audit',
data: { networkSpecific: true },
children: [
{
path: ':id',
component: BlockAuditComponent
},
],
},
{ {
path: 'docs', path: 'docs',
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
@ -658,7 +627,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, { imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabled', initialNavigation: 'enabledBlocking',
scrollPositionRestoration: 'enabled', scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled', anchorScrolling: 'enabled',
preloadingStrategy: AppPreloadingStrategy preloadingStrategy: AppPreloadingStrategy

View File

@ -10,27 +10,27 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="mb-3 radio-form"> <form [formGroup]="radioGroupForm" class="mb-3 radio-form">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="interval"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
<input ngbButton type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')"> 30M <input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
<input ngbButton type="radio" [value]="'hour'" (click)="setFragment('hour')"> 1H <input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
<input ngbButton type="radio" [value]="'half_day'" (click)="setFragment('half_day')"> 12H <input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
<input ngbButton type="radio" [value]="'day'" (click)="setFragment('day')"> 1D <input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
<input ngbButton type="radio" [value]="'week'" (click)="setFragment('week')"> 1W <input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
<input ngbButton type="radio" [value]="'month'" (click)="setFragment('month')"> 1M <input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
<input ngbButton type="radio" [value]="'year'" (click)="setFragment('year')"> 1Y <input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 1Y
</label> </label>
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, merge, Observable, of } from 'rxjs'; import { combineLatest, merge, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
@ -19,7 +19,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
currency$: Observable<any>; currency$: Observable<any>;
offers$: Observable<OffersMarket>; offers$: Observable<OffersMarket>;
trades$: Observable<Trade[]>; trades$: Observable<Trade[]>;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
defaultInterval = 'day'; defaultInterval = 'day';
isLoadingGraph = false; isLoadingGraph = false;
@ -28,7 +28,7 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private route: ActivatedRoute, private route: ActivatedRoute,
private bisqApiService: BisqApiService, private bisqApiService: BisqApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private seoService: SeoService, private seoService: SeoService,
private router: Router, private router: Router,
) { } ) { }

View File

@ -5,7 +5,7 @@ import { Observable, Subscription } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators'; import { switchMap, map, tap } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service'; import { BisqApiService } from '../bisq-api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { FormGroup, FormBuilder } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types' import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
@ -23,7 +23,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
fiveItemsPxSize = 250; fiveItemsPxSize = 250;
isLoading = true; isLoading = true;
loadingItems: number[]; loadingItems: number[];
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
types: string[] = []; types: string[] = [];
radioGroupSubscription: Subscription; radioGroupSubscription: Subscription;
@ -70,7 +70,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private bisqApiService: BisqApiService, private bisqApiService: BisqApiService,
private seoService: SeoService, private seoService: SeoService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,

View File

@ -129,7 +129,7 @@
<span>Gemini</span> <span>Gemini</span>
</a> </a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="81" height="81" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/> <circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)"> <g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/> <path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>

View File

@ -3,8 +3,8 @@
text-align: center; text-align: center;
.image { .image {
width: 81px; width: 80px;
height: 81px; height: 80px;
background-size: 100%, 100%; background-size: 100%, 100%;
border-radius: 50%; border-radius: 50%;
margin: 25px; margin: 25px;
@ -191,6 +191,6 @@
} }
.community-integrations-sponsor { .community-integrations-sponsor {
max-width: 970px; max-width: 965px;
margin: auto; margin: auto;
} }

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs'; import { merge, Observable, of, Subject } from 'rxjs';
@ -19,7 +19,7 @@ import { environment } from '../../../../environments/environment';
export class AssetsNavComponent implements OnInit { export class AssetsNavComponent implements OnInit {
@ViewChild('instance', {static: true}) instance: NgbTypeahead; @ViewChild('instance', {static: true}) instance: NgbTypeahead;
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
searchForm: FormGroup; searchForm: UntypedFormGroup;
assetsCache: AssetExtended[]; assetsCache: AssetExtended[];
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>); typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
@ -30,7 +30,7 @@ export class AssetsNavComponent implements OnInit {
itemsPerPage = 15; itemsPerPage = 15;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private seoService: SeoService, private seoService: SeoService,
private router: Router, private router: Router,
private assetsService: AssetsService, private assetsService: AssetsService,

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '../../services/assets.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
@ -22,7 +22,7 @@ export class AssetsComponent implements OnInit {
assets: AssetExtended[]; assets: AssetExtended[];
assetsCache: AssetExtended[]; assetsCache: AssetExtended[];
searchForm: FormGroup; searchForm: UntypedFormGroup;
assets$: Observable<AssetExtended[]>; assets$: Observable<AssetExtended[]>;
page = 1; page = 1;

View File

@ -30,7 +30,6 @@ export class BisqMasterPageComponent implements OnInit {
this.connectionState$ = this.stateService.connectionState$; this.connectionState$ = this.stateService.connectionState$;
this.urlLanguage = this.languageService.getLanguageForUrl(); this.urlLanguage = this.languageService.getLanguageForUrl();
this.navigationService.subnetPaths.subscribe((paths) => { this.navigationService.subnetPaths.subscribe((paths) => {
console.log('network paths updated...');
this.networkPaths = paths; this.networkPaths = paths;
}); });
} }

View File

@ -1,172 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<div class="title-block" id="block">
<h1>
<span class="next-previous-blocks">
<span i18n="shared.block-audit-title">Block Audit</span>
&nbsp;
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
&nbsp;
</span>
</h1>
<div class="grow"></div>
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">&#10005;</button>
</div>
<div *ngIf="!error && !isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
</td>
</tr>
<tr>
<td i18n="blockAudit.timestamp">Timestamp</td>
<td>
&lrm;{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time-since [time]="blockAudit.timestamp" [fastRender]="true">
</app-time-since>)</i>
</div>
</td>
</tr>
<tr>
<td i18n="blockAudit.size">Size</td>
<td [innerHTML]="'&lrm;' + (blockAudit.size | bytes: 2)"></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (blockAudit.weight | wuBytes: 2)"></td>
</tr>
</tbody>
</table>
</div>
<!-- RIGHT COLUMN -->
<div class="col-sm" *ngIf="blockAudit">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
<td>{{ blockAudit.tx_count }}</td>
</tr>
<tr>
<td i18n="block.health">Block health</td>
<td>{{ blockAudit.matchRate }}%</td>
</tr>
<tr>
<td i18n="block.missing-txs">Removed txs</td>
<td>{{ blockAudit.missingTxs.length }}</td>
</tr>
<tr>
<td i18n="block.added-txs">Added txs</td>
<td>{{ blockAudit.addedTxs.length }}</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- row -->
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</div>
<ng-template [ngIf]="!error && isLoading">
<!-- OVERVIEW -->
<div class="box mb-3">
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
<!-- RIGHT COLUMN -->
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
</tbody>
</table>
</div>
</div> <!-- row -->
</div> <!-- box -->
<!-- ADDED vs MISSING button -->
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div *ngIf="error && error.status === 404; else generalError" class="text-center">
<br>
<b i18n="error.audit-unavailable">audit unavailable</b>
<br><br>
<i>{{ error.error }}</i>
<br>
<br>
</div>
<ng-template #generalError>
<div class="text-center">
<br>
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error }}</i>
<br>
<br>
</div>
</ng-template>
</ng-template>
<!-- VISUALIZATIONS -->
<div class="box" *ngIf="!error">
<div class="row">
<!-- MISSING TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
</div>
<!-- ADDED TX RENDERING -->
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
</div>
</div> <!-- row -->
</div> <!-- box -->
</div>

View File

@ -1,44 +0,0 @@
.title-block {
border-top: none;
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 768px) {
text-align: left;
}
}
}
}
.block-tx-title {
display: flex;
justify-content: space-between;
flex-direction: column;
position: relative;
@media (min-width: 550px) {
flex-direction: row;
}
h2 {
line-height: 1;
margin: 0;
position: relative;
padding-bottom: 10px;
@media (min-width: 550px) {
padding-bottom: 0px;
align-self: end;
}
}
}
.menu-button {
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}

View File

@ -1,229 +0,0 @@
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription, combineLatest, of } from 'rxjs';
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { StateService } from '../../services/state.service';
import { detectWebGL } from '../../shared/graphs.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
@Component({
selector: 'app-block-audit',
templateUrl: './block-audit.component.html',
styleUrls: ['./block-audit.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
})
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
blockAudit: BlockAudit = undefined;
transactions: string[];
auditSubscription: Subscription;
urlFragmentSubscription: Subscription;
paginationMaxSize: number;
page = 1;
itemsPerPage: number;
mode: 'projected' | 'actual' = 'projected';
error: any;
isLoading = true;
webGlEnabled = true;
isMobile = window.innerWidth <= 767.98;
hoverTx: string;
childChangeSubscription: Subscription;
blockHash: string;
numMissing: number = 0;
numUnexpected: number = 0;
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
public stateService: StateService,
private router: Router,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
) {
this.webGlEnabled = detectWebGL();
}
ngOnDestroy() {
this.childChangeSubscription.unsubscribe();
this.urlFragmentSubscription.unsubscribe();
}
ngOnInit(): void {
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
if (fragment === 'actual') {
this.mode = 'actual';
} else {
this.mode = 'projected'
}
this.setupBlockGraphs();
});
this.auditSubscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const blockHash = params.get('id') || null;
if (!blockHash) {
return null;
}
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash: string) => {
if (hash) {
this.blockHash = hash;
return this.apiService.getBlockAudit$(this.blockHash)
} else {
return null;
}
}),
catchError((err) => {
this.error = err;
return of(null);
}),
);
}
return this.apiService.getBlockAudit$(this.blockHash)
}),
filter((response) => response != null),
map((response) => {
const blockAudit = response.body;
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
this.numMissing = 0;
this.numUnexpected = 0;
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoading = false;
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoading = false;
});
}
ngAfterViewInit() {
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
})
}
setupBlockGraphs() {
if (this.blockAudit) {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit.transactions);
} else {
graph.setup(this.blockAudit.template);
}
})
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit.transactions);
})
}
}
onResize(event: any) {
const isMobile = event.target.innerWidth <= 767.98;
const changed = isMobile !== this.isMobile;
this.isMobile = isMobile;
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
if (changed) {
this.changeMode(this.mode);
}
}
changeMode(mode: 'projected' | 'actual') {
this.router.navigate([], { fragment: mode });
}
onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]);
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@ -10,36 +10,36 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 24h <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 24h
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3D <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3D
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1W <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
@ -33,7 +33,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -50,7 +50,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private stateService: StateService, private stateService: StateService,

View File

@ -10,27 +10,27 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
@ -31,7 +31,7 @@ export class BlockFeesGraphComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -48,7 +48,7 @@ export class BlockFeesGraphComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -1,7 +1,8 @@
<div class="block-overview-graph"> <div class="block-overview-graph">
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas> <canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="!isLoading || disableSpinner"> <div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div class="spinner-border ml-3 loading" role="status"></div> <div *ngIf="isLoading" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div> </div>
<app-block-overview-tooltip <app-block-overview-tooltip

View File

@ -19,6 +19,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() flip = true; @Input() flip = true;
@Input() disableSpinner = false; @Input() disableSpinner = false;
@Input() mirrorTxid: string | void; @Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@Output() txClickEvent = new EventEmitter<TransactionStripped>(); @Output() txClickEvent = new EventEmitter<TransactionStripped>();
@Output() txHoverEvent = new EventEmitter<string>(); @Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter();

View File

@ -3,12 +3,13 @@ import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { feeLevels, mempoolFeeColors } from '../../app.constants';
import BlockScene from './block-scene';
const hoverTransitionTime = 300; const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4'); const defaultHoverColor = hexToColor('1bd8f4');
const feeColors = mempoolFeeColors.map(hexToColor); const feeColors = mempoolFeeColors.map(hexToColor);
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3)); const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
const auditColors = { const auditColors = {
censored: hexToColor('f344df'), censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
@ -34,7 +35,8 @@ export default class TxView implements TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
feerate: number; feerate: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
initialised: boolean; initialised: boolean;
vertexArray: FastVertexArray; vertexArray: FastVertexArray;
@ -48,6 +50,7 @@ export default class TxView implements TransactionStripped {
dirty: boolean; dirty: boolean;
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) { constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
this.context = tx.context;
this.txid = tx.txid; this.txid = tx.txid;
this.fee = tx.fee; this.fee = tx.fee;
this.vsize = tx.vsize; this.vsize = tx.vsize;
@ -159,12 +162,18 @@ export default class TxView implements TransactionStripped {
return auditColors.censored; return auditColors.censored;
case 'missing': case 'missing':
return auditColors.missing; return auditColors.missing;
case 'fresh':
return auditColors.missing;
case 'added': case 'added':
return auditColors.added; return auditColors.added;
case 'selected': case 'selected':
return auditColors.selected; return auditColors.selected;
case 'found': case 'found':
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
} else {
return feeLevelColor;
}
default: default:
return feeLevelColor; return feeLevelColor;
} }

View File

@ -37,9 +37,10 @@
<ng-container [ngSwitch]="tx?.status"> <ng-container [ngSwitch]="tx?.status">
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td> <td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td> <td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
<td *ngSwitchCase="'missing'" i18n="transaction.audit.omitted">omitted</td> <td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td> <td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
<td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td> <td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

@ -10,36 +10,36 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 24h <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 24h
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3D <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3D
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1W <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount > 157680" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-prediction' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -31,7 +31,7 @@ export class BlockPredictionGraphComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -48,7 +48,7 @@ export class BlockPredictionGraphComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private zone: NgZone, private zone: NgZone,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -11,27 +11,27 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
@ -31,7 +31,7 @@ export class BlockRewardsGraphComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -48,7 +48,7 @@ export class BlockRewardsGraphComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private miningService: MiningService, private miningService: MiningService,
private storageService: StorageService, private storageService: StorageService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -9,36 +9,36 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(blockSizesWeightsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 24h <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 24h
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3D <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3D
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1W <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@ -30,7 +30,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -49,7 +49,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -2,7 +2,7 @@
<div class="title-block" [class.time-ltr]="timeLtr" id="block"> <div class="title-block" [class.time-ltr]="timeLtr" id="block">
<h1> <h1>
<ng-container *ngIf="blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container> <ng-container *ngIf="blockHeight == null || blockHeight > 0; else genesis" i18n="shared.block-title">Block</ng-container>
<ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template> <ng-template #genesis i18n="@@2303359202781425764">Genesis</ng-template>
<span class="next-previous-blocks"> <span class="next-previous-blocks">
<a *ngIf="showNextBlocklink" class="nav-arrow next" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom"> <a *ngIf="showNextBlocklink" class="nav-arrow next" [routerLink]="['/block/' | relativeUrl, nextBlockHeight]" (click)="navigateToNextBlock()" i18n-ngbTooltip="Next Block" ngbTooltip="Next Block" placement="bottom">
@ -54,7 +54,19 @@
<td i18n="block.weight">Weight</td> <td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td> <td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr> </tr>
<ng-template [ngIf]="webGlEnabled"> <tr *ngIf="auditEnabled">
<td i18n="block.health">Block health</td>
<td>
<span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
<span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
</td>
</tr>
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
<tr *ngIf="isMobile && auditEnabled"></tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined"> <tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td> <td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
@ -98,26 +110,19 @@
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.miner">Miner</td> <td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD"> <td *ngIf="stateService.env.MINING_DASHBOARD">
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }} {{ block.extras.pool.name }}
</a> </a>
</td> </td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'"> <td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge" <span placement="bottom" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> [class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }} {{ block.extras.pool.name }}
</span> </span>
</td> </td>
</tr> </tr>
<tr *ngIf="indexingAvailable"> </ng-container>
<td i18n="block.health">Block health</td>
<td>
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
</td>
</tr>
</ng-template>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -138,7 +143,11 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<ng-template [ngIf]="webGlEnabled"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<ng-container *ngIf="!indexingAvailable && webGlEnabled">
<tr *ngIf="isMobile && !auditEnabled"></tr>
<tr> <tr>
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
@ -148,17 +157,25 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<tr> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
</ng-template> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</ng-container>
</tbody> </tbody>
</table> </table>
</div> </div>
</ng-template> </ng-template>
<div class="col-sm" *ngIf="!webGlEnabled"> <div class="col-sm">
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock"> <table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (indexingAvailable || !webGlEnabled)">
<tbody> <tbody>
<tr *ngIf="isMobile && auditEnabled"></tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr *ngIf="block?.extras?.medianFee != undefined"> <tr *ngIf="block?.extras?.medianFee != undefined">
<td class="td-width" i18n="block.median-fee">Median fee</td> <td class="td-width" i18n="block.median-fee">Median fee</td>
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td> <td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
@ -216,8 +233,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock"> <table class="table table-borderless table-striped" *ngIf="isLoadingBlock && (indexingAvailable || !webGlEnabled)">
<tbody> <tbody>
<tr *ngIf="isMobile && !auditEnabled"></tr>
<tr> <tr>
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td> <td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
@ -230,22 +248,54 @@
<tr> <tr>
<td colspan="2"><span class="skeleton-loader"></span></td> <td colspan="2"><span class="skeleton-loader"></span></td>
</tr> </tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> <div class="col-sm chart-container" *ngIf="webGlEnabled && !indexingAvailable">
<div class="col-sm chart-container" *ngIf="webGlEnabled"> <app-block-overview-graph
<app-block-overview-graph #blockGraphActual
#blockGraph [isLoading]="isLoadingOverview"
[isLoading]="isLoadingOverview" [resolution]="75"
[resolution]="75" [blockLimit]="stateService.blockVSize"
[blockLimit]="stateService.blockVSize" [orientation]="'top'"
[orientation]="'top'" [flip]="false"
[flip]="false" (txClickEvent)="onTxClick($event)"
(txClickEvent)="onTxClick($event)" ></app-block-overview-graph>
></app-block-overview-graph> </div>
</div> </div>
</div> </div>
</div> </div>
<span id="overview"></span>
<br>
<!-- VISUALIZATIONS -->
<div class="box" *ngIf="!error && webGlEnabled && indexingAvailable">
<div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
<a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
fragment="projected" (click)="changeMode('projected')">Projected</a>
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a>
</div>
<div class="row">
<div class="col-sm">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph>
</div>
<div class="col-sm" *ngIf="!isMobile">
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph>
</div>
</div>
</div>
<ng-template [ngIf]="!isLoadingBlock && !error"> <ng-template [ngIf]="!isLoadingBlock && !error">
<div [hidden]="!showDetails" id="details"> <div [hidden]="!showDetails" id="details">
<br> <br>
@ -273,6 +323,7 @@
<div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <div class="col-sm" *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr *ngIf="isMobile"></tr>
<tr> <tr>
<td class="td-width" i18n="block.difficulty">Difficulty</td> <td class="td-width" i18n="block.difficulty">Difficulty</td>
<td>{{ block.difficulty }}</td> <td>{{ block.difficulty }}</td>

View File

@ -171,3 +171,35 @@ h1 {
margin: auto; margin: auto;
} }
} }
.menu-button {
@media (min-width: 768px) {
max-width: 150px;
}
}
.block-subtitle {
text-align: center;
}
.nav-tabs {
border-color: white;
border-width: 1px;
}
.nav-tabs .nav-link {
background: inherit;
border-width: 1px;
border-bottom: none;
border-color: transparent;
margin-bottom: -1px;
cursor: pointer;
&.active {
background: #24273e;
}
&.active, &:hover {
border-color: white;
}
}

View File

@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs'; import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils'; import { detectWebGL } from '../../shared/graphs.utils';
@ -17,11 +17,20 @@ import { detectWebGL } from '../../shared/graphs.utils';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
templateUrl: './block.component.html', templateUrl: './block.component.html',
styleUrls: ['./block.component.scss'] styleUrls: ['./block.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
}) })
export class BlockComponent implements OnInit, OnDestroy { export class BlockComponent implements OnInit, OnDestroy {
network = ''; network = '';
block: BlockExtended; block: BlockExtended;
blockAudit: BlockAudit = undefined;
blockHeight: number; blockHeight: number;
lastBlockHeight: number; lastBlockHeight: number;
nextBlockHeight: number; nextBlockHeight: number;
@ -48,9 +57,16 @@ export class BlockComponent implements OnInit, OnDestroy {
overviewError: any = null; overviewError: any = null;
webGlEnabled = true; webGlEnabled = true;
indexingAvailable = false; indexingAvailable = false;
auditEnabled = true;
isMobile = window.innerWidth <= 767.98;
hoverTx: string;
numMissing: number = 0;
numUnexpected: number = 0;
mode: 'projected' | 'actual' = 'projected';
transactionSubscription: Subscription; transactionSubscription: Subscription;
overviewSubscription: Subscription; overviewSubscription: Subscription;
auditSubscription: Subscription;
keyNavigationSubscription: Subscription; keyNavigationSubscription: Subscription;
blocksSubscription: Subscription; blocksSubscription: Subscription;
networkChangedSubscription: Subscription; networkChangedSubscription: Subscription;
@ -60,10 +76,10 @@ export class BlockComponent implements OnInit, OnDestroy {
nextBlockTxListSubscription: Subscription = undefined; nextBlockTxListSubscription: Subscription = undefined;
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
fetchAuditScore$ = new Subject<string>(); childChangeSubscription: Subscription;
fetchAuditScoreSubscription: Subscription;
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -89,8 +105,8 @@ export class BlockComponent implements OnInit, OnDestroy {
this.timeLtr = !!ltr; this.timeLtr = !!ltr;
}); });
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
this.stateService.env.MINING_DASHBOARD === true); this.auditEnabled = this.indexingAvailable;
this.txsLoadingStatus$ = this.route.paramMap this.txsLoadingStatus$ = this.route.paramMap
.pipe( .pipe(
@ -107,30 +123,12 @@ export class BlockComponent implements OnInit, OnDestroy {
if (block.id === this.blockHash) { if (block.id === this.blockHash) {
this.block = block; this.block = block;
if (this.block.id && this.block?.extras?.matchRate == null) {
this.fetchAuditScore$.next(this.block.id);
}
if (block?.extras?.reward != undefined) { if (block?.extras?.reward != undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy; this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
} }
} }
}); });
if (this.indexingAvailable) {
this.fetchAuditScoreSubscription = this.fetchAuditScore$
.pipe(
switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
catchError(() => EMPTY),
)
.subscribe((score) => {
if (score && score.hash === this.block.id) {
this.block.extras.matchRate = score.matchRate || null;
} else {
this.block.extras.matchRate = null;
}
});
}
const block$ = this.route.paramMap.pipe( const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
const blockHash: string = params.get('id') || ''; const blockHash: string = params.get('id') || '';
@ -212,7 +210,7 @@ export class BlockComponent implements OnInit, OnDestroy {
setTimeout(() => { setTimeout(() => {
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe(); this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe(); this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe(); this.apiService.getBlockAudit$(block.previousblockhash);
}, 100); }, 100);
} }
@ -229,9 +227,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy; this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
} }
this.stateService.markBlock$.next({ blockHeight: this.blockHeight }); this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
if (this.block.id && this.block?.extras?.matchRate == null) {
this.fetchAuditScore$.next(this.block.id);
}
this.isLoadingTransactions = true; this.isLoadingTransactions = true;
this.transactions = null; this.transactions = null;
this.transactionsError = null; this.transactionsError = null;
@ -263,40 +258,126 @@ export class BlockComponent implements OnInit, OnDestroy {
this.isLoadingOverview = false; this.isLoadingOverview = false;
}); });
this.overviewSubscription = block$.pipe( if (!this.indexingAvailable) {
startWith(null), this.overviewSubscription = block$.pipe(
pairwise(), startWith(null),
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id) pairwise(),
.pipe( switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
catchError((err) => { .pipe(
this.overviewError = err; catchError((err) => {
return of([]); this.overviewError = err;
}), return of([]);
switchMap((transactions) => { }),
if (prevBlock) { switchMap((transactions) => {
return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' }); if (prevBlock) {
} else { return of({ transactions, direction: (prevBlock.height < block.height) ? 'right' : 'left' });
return of({ transactions, direction: 'down' }); } else {
return of({ transactions, direction: 'down' });
}
})
)
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
this.strippedTransactions = transactions;
this.isLoadingOverview = false;
this.setupBlockGraphs();
},
(error) => {
this.error = error;
this.isLoadingOverview = false;
});
}
if (this.indexingAvailable) {
this.auditSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
return of([]);
})
)
),
filter((response) => response != null),
map((response) => {
const blockAudit = response.body;
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
} }
}) for (const tx of blockAudit.transactions) {
) inBlock[tx.txid] = true;
), }
) for (const txid of blockAudit.addedTxs) {
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { isAdded[txid] = true;
this.strippedTransactions = transactions; }
this.isLoadingOverview = false; for (const txid of blockAudit.missingTxs) {
if (this.blockGraph) { isCensored[txid] = true;
this.blockGraph.destroy(); }
this.blockGraph.setup(this.strippedTransactions); for (const txid of blockAudit.freshTxs || []) {
} isFresh[txid] = true;
}, }
(error) => { // set transaction statuses
this.error = error; for (const tx of blockAudit.template) {
this.isLoadingOverview = false; tx.context = 'projected';
if (this.blockGraph) { if (isCensored[tx.txid]) {
this.blockGraph.destroy(); tx.status = 'censored';
} } else if (inBlock[tx.txid]) {
}); tx.status = 'found';
} else {
tx.status = isFresh[tx.txid] ? 'fresh' : 'missing';
isMissing[tx.txid] = true;
this.numMissing++;
}
}
for (const [index, tx] of blockAudit.transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
}
for (const tx of blockAudit.transactions) {
inBlock[tx.txid] = true;
}
this.auditEnabled = true;
} else {
this.auditEnabled = false;
}
return blockAudit;
}),
catchError((err) => {
console.log(err);
this.error = err;
this.isLoadingOverview = false;
return of(null);
}),
).subscribe((blockAudit) => {
this.blockAudit = blockAudit;
this.setupBlockGraphs();
this.isLoadingOverview = false;
});
}
this.networkChangedSubscription = this.stateService.networkChanged$ this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network); .subscribe((network) => this.network = network);
@ -307,6 +388,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else { } else {
this.showDetails = false; this.showDetails = false;
} }
if (params.view === 'projected') {
this.mode = 'projected';
} else {
this.mode = 'actual';
}
this.setupBlockGraphs();
}); });
this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => { this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => {
@ -325,17 +412,24 @@ export class BlockComponent implements OnInit, OnDestroy {
}); });
} }
ngAfterViewInit(): void {
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy() { ngOnDestroy() {
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.transactionSubscription.unsubscribe(); this.transactionSubscription.unsubscribe();
this.overviewSubscription.unsubscribe(); this.overviewSubscription?.unsubscribe();
this.auditSubscription?.unsubscribe();
this.keyNavigationSubscription.unsubscribe(); this.keyNavigationSubscription.unsubscribe();
this.blocksSubscription.unsubscribe(); this.blocksSubscription.unsubscribe();
this.networkChangedSubscription.unsubscribe(); this.networkChangedSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe();
this.fetchAuditScoreSubscription?.unsubscribe();
this.unsubscribeNextBlockSubscriptions(); this.unsubscribeNextBlockSubscriptions();
this.childChangeSubscription.unsubscribe();
} }
unsubscribeNextBlockSubscriptions() { unsubscribeNextBlockSubscriptions() {
@ -382,7 +476,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.showDetails = false; this.showDetails = false;
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { showDetails: false }, queryParams: { showDetails: false, view: this.mode },
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'block' fragment: 'block'
}); });
@ -390,7 +484,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.showDetails = true; this.showDetails = true;
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { showDetails: true }, queryParams: { showDetails: true, view: this.mode },
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: 'details' fragment: 'details'
}); });
@ -409,10 +503,6 @@ export class BlockComponent implements OnInit, OnDestroy {
return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000; return this.block && this.block.height > 681393 && (new Date().getTime() / 1000) < 1628640000;
} }
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
navigateToPreviousBlock() { navigateToPreviousBlock() {
if (!this.block) { if (!this.block) {
return; return;
@ -443,8 +533,53 @@ export class BlockComponent implements OnInit, OnDestroy {
} }
} }
setupBlockGraphs(): void {
if (this.blockAudit || this.strippedTransactions) {
this.blockGraphProjected.forEach(graph => {
graph.destroy();
if (this.isMobile && this.mode === 'actual') {
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
} else {
graph.setup(this.blockAudit?.template || []);
}
});
this.blockGraphActual.forEach(graph => {
graph.destroy();
graph.setup(this.blockAudit?.transactions || this.strippedTransactions || []);
});
}
}
onResize(event: any): void {
const isMobile = event.target.innerWidth <= 767.98;
const changed = isMobile !== this.isMobile;
this.isMobile = isMobile;
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
if (changed) {
this.changeMode(this.mode);
}
}
changeMode(mode: 'projected' | 'actual'): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { showDetails: this.showDetails, view: mode },
queryParamsHandling: 'merge',
fragment: 'overview'
});
}
onTxClick(event: TransactionStripped): void { onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`); const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
this.router.navigate([url]); this.router.navigate([url]);
} }
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
} }

View File

@ -46,14 +46,13 @@
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> <app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
</td> </td>
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null"> <a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block/' | relativeUrl, block.id] : null">
<div class="progress progress-health"> <div class="progress progress-health">
<div class="progress-bar progress-bar-health" role="progressbar" <div class="progress-bar progress-bar-health" role="progressbar"
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div> [ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
<div class="progress-text"> <div class="progress-text">
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span> <span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
<span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span> <span *ngIf="auditScores[block.id] == null">~</span>
<span *ngIf="auditScores[block.id] === null">~</span>
</div> </div>
</div> </div>
</a> </a>

View File

@ -31,24 +31,24 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3m'"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3m'" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'6m'"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'all'"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@ import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { selectPowerOfTen } from '../../bitcoin.utils'; import { selectPowerOfTen } from '../../bitcoin.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
@ -34,7 +34,7 @@ export class HashrateChartComponent implements OnInit {
@Input() left: number | string = 75; @Input() left: number | string = 75;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -54,7 +54,7 @@ export class HashrateChartComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private route: ActivatedRoute, private route: ActivatedRoute,

View File

@ -11,21 +11,21 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'6m'"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.blockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'all'"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { poolsColor } from '../../app.constants'; import { poolsColor } from '../../app.constants';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
@ -30,7 +30,7 @@ export class HashrateChartPoolsComponent implements OnInit {
@Input() left: number | string = 25; @Input() left: number | string = 25;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -48,7 +48,7 @@ export class HashrateChartPoolsComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,

View File

@ -1,6 +1,6 @@
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { languages } from '../../app.constants'; import { languages } from '../../app.constants';
import { LanguageService } from '../../services/language.service'; import { LanguageService } from '../../services/language.service';
@ -11,12 +11,12 @@ import { LanguageService } from '../../services/language.service';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class LanguageSelectorComponent implements OnInit { export class LanguageSelectorComponent implements OnInit {
languageForm: FormGroup; languageForm: UntypedFormGroup;
languages = languages; languages = languages;
constructor( constructor(
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private languageService: LanguageService, private languageService: LanguageService,
) { } ) { }

View File

@ -33,7 +33,6 @@ export class LiquidMasterPageComponent implements OnInit {
this.network$ = merge(of(''), this.stateService.networkChanged$); this.network$ = merge(of(''), this.stateService.networkChanged$);
this.urlLanguage = this.languageService.getLanguageForUrl(); this.urlLanguage = this.languageService.getLanguageForUrl();
this.navigationService.subnetPaths.subscribe((paths) => { this.navigationService.subnetPaths.subscribe((paths) => {
console.log('network paths updated...');
this.networkPaths = paths; this.networkPaths = paths;
}); });
} }

View File

@ -35,7 +35,6 @@ export class MasterPageComponent implements OnInit {
this.urlLanguage = this.languageService.getLanguageForUrl(); this.urlLanguage = this.languageService.getLanguageForUrl();
this.subdomain = this.enterpriseService.getSubdomain(); this.subdomain = this.enterpriseService.getSubdomain();
this.navigationService.subnetPaths.subscribe((paths) => { this.navigationService.subnetPaths.subscribe((paths) => {
console.log('network paths updated...');
this.networkPaths = paths; this.networkPaths = paths;
}); });
} }

View File

@ -17,8 +17,8 @@ import {
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormBuilder, UntypedFormBuilder,
FormControl, UntypedFormControl,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validator, Validator,
} from '@angular/forms'; } from '@angular/forms';
@ -52,7 +52,7 @@ export class NgxDropdownMultiselectComponent implements OnInit,
private localIsVisible = false; private localIsVisible = false;
private workerDocClicked = false; private workerDocClicked = false;
filterControl: FormControl = this.fb.control(''); filterControl: UntypedFormControl = this.fb.control('');
@Input() options: Array<IMultiSelectOption>; @Input() options: Array<IMultiSelectOption>;
@Input() settings: IMultiSelectSettings; @Input() settings: IMultiSelectSettings;
@ -151,7 +151,7 @@ export class NgxDropdownMultiselectComponent implements OnInit,
} }
constructor( constructor(
private fb: FormBuilder, private fb: UntypedFormBuilder,
private searchFilter: MultiSelectSearchFilter, private searchFilter: MultiSelectSearchFilter,
differs: IterableDiffers, differs: IterableDiffers,
private cdRef: ChangeDetectorRef private cdRef: ChangeDetectorRef

View File

@ -40,36 +40,36 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" <form [formGroup]="radioGroupForm" class="formRadioGroup"
*ngIf="!widget && (miningStatsObservable$ | async) as stats"> *ngIf="!widget && (miningStatsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 144"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 144" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'24h'"> 24h <input type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'24h'" formControlName="dateSpan"> 24h
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 432"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 432" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3d'"> 3D <input type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3d'" formControlName="dateSpan"> 3D
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 1008"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 1008" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1w'"> 1W <input type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1w'" formControlName="dateSpan"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 4320"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 4320" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1m'"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1m'" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 12960"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 12960" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3m'"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3m'" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 25920"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 25920" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'6m'"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'6m'" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 52560"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 52560" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1y'"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'1y'" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 105120"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 105120" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'2y'"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'2y'" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680"> <label class="btn btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3y'"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'3y'" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'all'"><span i18n>All</span> <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]" [attr.data-cy]="'all'" formControlName="dateSpan"><span i18n>All</span>
</label> </label>
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts'; import { EChartsOption, PieSeriesOption } from 'echarts';
import { concat, Observable } from 'rxjs'; import { concat, Observable } from 'rxjs';
@ -24,7 +24,7 @@ export class PoolRankingComponent implements OnInit {
@Input() widget = false; @Input() widget = false;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
isLoading = true; isLoading = true;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
@ -41,7 +41,7 @@ export class PoolRankingComponent implements OnInit {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private storageService: StorageService, private storageService: StorageService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private miningService: MiningService, private miningService: MiningService,
private seoService: SeoService, private seoService: SeoService,
private router: Router, private router: Router,

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
@Component({ @Component({
@ -8,13 +8,13 @@ import { ApiService } from '../../services/api.service';
styleUrls: ['./push-transaction.component.scss'] styleUrls: ['./push-transaction.component.scss']
}) })
export class PushTransactionComponent implements OnInit { export class PushTransactionComponent implements OnInit {
pushTxForm: FormGroup; pushTxForm: UntypedFormGroup;
error: string = ''; error: string = '';
txId: string = ''; txId: string = '';
isLoading = false; isLoading = false;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private apiService: ApiService, private apiService: ApiService,
) { } ) { }

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '../../services/assets.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@ -22,7 +22,7 @@ export class SearchFormComponent implements OnInit {
isSearching = false; isSearching = false;
isTypeaheading$ = new BehaviorSubject<boolean>(false); isTypeaheading$ = new BehaviorSubject<boolean>(false);
typeAhead$: Observable<any>; typeAhead$: Observable<any>;
searchForm: FormGroup; searchForm: UntypedFormGroup;
dropdownHidden = false; dropdownHidden = false;
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
@ -48,7 +48,7 @@ export class SearchFormComponent implements OnInit {
} }
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private router: Router, private router: Router,
private assetsService: AssetsService, private assetsService: AssetsService,
private stateService: StateService, private stateService: StateService,

View File

@ -13,46 +13,46 @@
<form [formGroup]="radioGroupForm" class="formRadioGroup" <form [formGroup]="radioGroupForm" class="formRadioGroup"
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()"> [class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle"> <div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
<label ngbButtonLabel class="btn-primary btn-sm mr-2"> <label class="btn btn-primary btn-sm mr-2">
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv"> <a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon> <fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a> </a>
</label> </label>
</div> </div>
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">
<input ngbButton type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h"> 2H <input type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h" formControlName="dateSpan"> 2H
(LIVE) (LIVE)
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '24h'">
<input ngbButton type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h"> <input type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h" formControlName="dateSpan">
24H 24H
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1w'">
<input ngbButton type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w"> 1W <input type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w" formControlName="dateSpan"> 1W
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m"> 1M <input type="radio" [value]="'1m'" [routerLink]="['/graphs' | relativeUrl]" fragment="1m" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m"> 3M <input type="radio" [value]="'3m'" [routerLink]="['/graphs' | relativeUrl]" fragment="3m" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m"> 6M <input type="radio" [value]="'6m'" [routerLink]="['/graphs' | relativeUrl]" fragment="6m" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y"> 1Y <input type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y"> 2Y <input type="radio" [value]="'2y'" [routerLink]="['/graphs' | relativeUrl]" fragment="2y" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y"> 3Y <input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
</label> </label>
</div> </div>
<div class="small-buttons"> <div class="small-buttons">
<div ngbDropdown #myDrop="ngbDropdown"> <div ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-primary btn-sm" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()"> <button class="btn btn btn-primary btn-sm" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()">
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title" <fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title"
title="Filter"></fa-icon> title="Filter"></fa-icon>
</button> </button>
@ -71,7 +71,7 @@
</div> </div>
</div> </div>
<button (click)="invertGraph()" class="btn btn-primary btn-sm"> <button (click)="invertGraph()" class="btn btn btn-primary btn-sm">
<fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true" <fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true"
i18n-title="statistics.component-invert.title" title="Invert"></fa-icon> i18n-title="statistics.component-invert.title" title="Invert"></fa-icon>
</button> </button>

View File

@ -1,6 +1,6 @@
import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core'; import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder } from '@angular/forms'; import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
import { of, merge} from 'rxjs'; import { of, merge} from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
@ -39,7 +39,7 @@ export class StatisticsComponent implements OnInit {
mempoolUnconfirmedTransactionsData: any; mempoolUnconfirmedTransactionsData: any;
mempoolTransactionsWeightPerSecondData: any; mempoolTransactionsWeightPerSecondData: any;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
graphWindowPreference: string; graphWindowPreference: string;
inverted: boolean; inverted: boolean;
feeLevelDropdownData = []; feeLevelDropdownData = [];
@ -47,7 +47,7 @@ export class StatisticsComponent implements OnInit {
constructor( constructor(
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private apiService: ApiService, private apiService: ApiService,

View File

@ -8,10 +8,10 @@
</div> </div>
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features"> <div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
<app-tx-features [tx]="tx"></app-tx-features> <app-tx-features [tx]="tx"></app-tx-features>
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1"> <span *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.descendants.length)" class="badge badge-primary mr-1">
CPFP CPFP
</span> </span>
<span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && cpfpInfo.ancestors.length" class="badge badge-info mr-1"> <span *ngIf="cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.descendants.length && cpfpInfo.ancestors.length" class="badge badge-info mr-1">
CPFP CPFP
</span> </span>
</div> </div>

View File

@ -63,34 +63,14 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.fetchCpfpSubscription = this.fetchCpfp$ this.fetchCpfpSubscription = this.fetchCpfp$
.pipe( .pipe(
switchMap((txId) => switchMap((txId) =>
this.apiService this.apiService.getCpfpinfo$(txId).pipe(
.getCpfpinfo$(txId) catchError((err) => {
.pipe(retryWhen((errors) => errors.pipe(delay(2000)))) return of(null);
})
)
) )
) )
.subscribe((cpfpInfo) => { .subscribe((cpfpInfo) => {
if (!this.tx) {
return;
}
const lowerFeeParents = cpfpInfo.ancestors.filter(
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
);
let totalWeight =
this.tx.weight +
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
if (cpfpInfo.bestDescendant) {
totalWeight += cpfpInfo.bestDescendant.weight;
totalFees += cpfpInfo.bestDescendant.fee;
}
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
});
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;
this.openGraphService.waitOver('cpfp-data-' + this.txId); this.openGraphService.waitOver('cpfp-data-' + this.txId);
}); });
@ -176,8 +156,17 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.getTransactionTime(); this.getTransactionTime();
} }
if (!this.tx.status.confirmed) { if (this.tx.status.confirmed) {
this.stateService.markBlock$.next({
blockHeight: tx.status.block_height,
});
this.openGraphService.waitFor('cpfp-data-' + this.txId);
this.fetchCpfp$.next(this.tx.txid);
} else {
if (tx.cpfpChecked) { if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txFeePerVSize: tx.effectiveFeePerVsize,
});
this.cpfpInfo = { this.cpfpInfo = {
ancestors: tx.ancestors, ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant, bestDescendant: tx.bestDescendant,

View File

@ -156,7 +156,20 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ng-template [ngIf]="cpfpInfo.bestDescendant"> <ng-template [ngIf]="cpfpInfo?.descendants?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
</a>
</td>
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr>
</ng-template>
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
<tr> <tr>
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td> <td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
<td> <td>
@ -170,7 +183,7 @@
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td> <td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
</tr> </tr>
</ng-template> </ng-template>
<ng-template [ngIf]="cpfpInfo.ancestors.length"> <ng-template [ngIf]="cpfpInfo?.ancestors?.length">
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors"> <tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td> <td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]"> <td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
@ -468,11 +481,11 @@
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
<ng-template [ngIf]="tx.status.confirmed"> <ng-template [ngIf]="tx.status.confirmed">
&nbsp; &nbsp;
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating> <app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo?.descendants?.length && !cpfpInfo?.bestDescendant && !cpfpInfo?.ancestors?.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
</ng-template> </ng-template>
</td> </td>
</tr> </tr>
<tr *ngIf="cpfpInfo && (cpfpInfo.bestDescendant || cpfpInfo.ancestors.length)"> <tr *ngIf="cpfpInfo && (cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length)">
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> <td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td> <td>
<div class="effective-fee-container"> <div class="effective-fee-container">

View File

@ -7,10 +7,11 @@ import {
catchError, catchError,
retryWhen, retryWhen,
delay, delay,
map map,
mergeMap
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs'; import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '../../services/audio.service';
@ -110,32 +111,51 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
switchMap((txId) => switchMap((txId) =>
this.apiService this.apiService
.getCpfpinfo$(txId) .getCpfpinfo$(txId)
.pipe(retryWhen((errors) => errors.pipe(delay(2000)))) .pipe(retryWhen((errors) => errors.pipe(
) mergeMap((error) => {
if (!this.tx?.status || this.tx.status.confirmed) {
return throwError(error);
} else {
return of(null);
}
}),
delay(2000)
)))
),
catchError(() => {
return of(null);
})
) )
.subscribe((cpfpInfo) => { .subscribe((cpfpInfo) => {
if (!this.tx) { if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
return; return;
} }
const lowerFeeParents = cpfpInfo.ancestors.filter( if (cpfpInfo.effectiveFeePerVsize) {
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
); } else {
let totalWeight = const lowerFeeParents = cpfpInfo.ancestors.filter(
this.tx.weight + (parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0); );
let totalFees = let totalWeight =
this.tx.fee + this.tx.weight +
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0); lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
if (cpfpInfo.bestDescendant) { if (cpfpInfo?.bestDescendant) {
totalWeight += cpfpInfo.bestDescendant.weight; totalWeight += cpfpInfo?.bestDescendant.weight;
totalFees += cpfpInfo.bestDescendant.fee; totalFees += cpfpInfo?.bestDescendant.fee;
}
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
}
if (!this.tx.status.confirmed) {
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
});
} }
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
});
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;
}); });
@ -239,6 +259,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.stateService.markBlock$.next({ this.stateService.markBlock$.next({
blockHeight: tx.status.block_height, blockHeight: tx.status.block_height,
}); });
this.fetchCpfp$.next(this.tx.txid);
} else { } else {
if (tx.cpfpChecked) { if (tx.cpfpChecked) {
this.stateService.markBlock$.next({ this.stateService.markBlock$.next({

View File

@ -31,6 +31,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
routes = [ routes = [
{ {
path: '', path: '',
pathMatch: 'full',
redirectTo: 'faq' redirectTo: 'faq'
}, },
{ {

View File

@ -139,6 +139,7 @@ const routes: Routes = [
}, },
{ {
path: '', path: '',
pathMatch: 'full',
redirectTo: 'mempool', redirectTo: 'mempool',
}, },
{ {

View File

@ -22,7 +22,9 @@ interface BestDescendant {
export interface CpfpInfo { export interface CpfpInfo {
ancestors: Ancestor[]; ancestors: Ancestor[];
bestDescendant: BestDescendant | null; descendants?: Ancestor[];
bestDescendant?: BestDescendant | null;
effectiveFeePerVsize?: number;
} }
export interface DifficultyAdjustment { export interface DifficultyAdjustment {
@ -141,7 +143,7 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
} }
export interface RewardStats { export interface RewardStats {
@ -215,8 +217,8 @@ export interface IChannel {
updated_at: string; updated_at: string;
created: string; created: string;
status: number; status: number;
node_left: Node, node_left: INode,
node_right: Node, node_right: INode,
} }
@ -234,4 +236,6 @@ export interface INode {
updated_at: string; updated_at: string;
longitude: number; longitude: number;
latitude: number; latitude: number;
funding_balance?: number;
closing_balance?: number;
} }

View File

@ -70,7 +70,8 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual';
} }
export interface IBackendInfo { export interface IBackendInfo {

View File

@ -0,0 +1,19 @@
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr></tr>
<tr>
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
<td *ngIf="!showStartingBalance">?</td>
</tr>
<tr *ngIf="channel.status === 2">
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
<td *ngIf="!showClosingBalance">?</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,9 @@
.box {
margin-top: 20px;
}
@media (max-width: 768px) {
.box {
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChannelCloseBoxComponent } from './channel-close-box.component';
describe('ChannelCloseBoxComponent', () => {
let component: ChannelCloseBoxComponent;
let fixture: ComponentFixture<ChannelCloseBoxComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ChannelCloseBoxComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ChannelCloseBoxComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-channel-close-box',
templateUrl: './channel-close-box.component.html',
styleUrls: ['./channel-close-box.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChannelCloseBoxComponent implements OnChanges {
@Input() channel: any;
@Input() local: any;
@Input() remote: any;
showStartingBalance: boolean = false;
showClosingBalance: boolean = false;
minStartingBalance: number;
maxStartingBalance: number;
minClosingBalance: number;
maxClosingBalance: number;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (this.channel && this.local && this.remote) {
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
if (this.channel.single_funded) {
if (this.local.funding_balance) {
this.minStartingBalance = this.channel.capacity;
this.maxStartingBalance = this.channel.capacity;
} else if (this.remote.funding_balance) {
this.minStartingBalance = 0;
this.maxStartingBalance = 0;
}
} else {
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
}
const closingCapacity = this.channel.capacity - this.channel.closing_fee;
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
// margin of error to account for 2 x 330 sat anchor outputs
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
this.maxClosingBalance = this.minClosingBalance;
}
} else {
this.showStartingBalance = false;
this.showClosingBalance = false;
}
}
}
function clampRound(min: number, max: number, value: number): number {
return Math.max(0, Math.min(max, Math.round(value)));
}

View File

@ -48,6 +48,15 @@
<td i18n="lightning.capacity">Capacity</td> <td i18n="lightning.capacity">Capacity</td>
<td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td> <td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td>
</tr> </tr>
<tr *ngIf="channel.closed_by">
<td i18n="lightning.closed_by">Closed by</td>
<td>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.closed_by]" >
<ng-container *ngIf="channel.closed_by === channel.node_left.public_key">{{ channel.node_left.alias }}</ng-container>
<ng-container *ngIf="channel.closed_by === channel.node_right.public_key">{{ channel.node_right.alias }}</ng-container>
</a>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -59,9 +68,11 @@
<div class="row row-cols-1 row-cols-md-2"> <div class="row row-cols-1 row-cols-md-2">
<div class="col"> <div class="col">
<app-channel-box [channel]="channel.node_left"></app-channel-box> <app-channel-box [channel]="channel.node_left"></app-channel-box>
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box>
</div> </div>
<div class="col"> <div class="col">
<app-channel-box [channel]="channel.node_right"></app-channel-box> <app-channel-box [channel]="channel.node_right"></app-channel-box>
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box>
</div> </div>
</div> </div>

View File

@ -78,4 +78,9 @@ export class ChannelComponent implements OnInit {
); );
} }
showCloseBoxes(channel: IChannel): boolean {
return !!(channel.node_left.funding_balance || channel.node_left.closing_balance
|| channel.node_right.funding_balance || channel.node_right.closing_balance);
}
} }

View File

@ -1,11 +1,11 @@
<div *ngIf="channels$ | async as response; else skeleton" style="position: relative;"> <div *ngIf="channels$ | async as response; else skeleton" style="position: relative;">
<form [formGroup]="channelStatusForm" class="formRadioGroup"> <form [formGroup]="channelStatusForm" class="formRadioGroup">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="channelStatusForm.get('status').value === 'open'">
<input ngbButton type="radio" [value]="'open'" fragment="open"><span i18n="open">Open</span> <input type="radio" [value]="'open'" fragment="open" formControlName="status"><span i18n="open">Open</span>
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="channelStatusForm.get('status').value === 'closed'">
<input ngbButton type="radio" [value]="'closed'" fragment="closed"><span i18n="closed">Closed</span> <input type="radio" [value]="'closed'" fragment="closed" formControlName="status"><span i18n="closed">Closed</span>
</label> </label>
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { BehaviorSubject, merge, Observable } from 'rxjs'; import { BehaviorSubject, merge, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators'; import { map, switchMap, tap } from 'rxjs/operators';
import { isMobile } from '../../shared/common.utils'; import { isMobile } from '../../shared/common.utils';
@ -23,7 +23,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
itemsPerPage = 10; itemsPerPage = 10;
page = 1; page = 1;
channelsPage$ = new BehaviorSubject<number>(1); channelsPage$ = new BehaviorSubject<number>(1);
channelStatusForm: FormGroup; channelStatusForm: UntypedFormGroup;
defaultStatus = 'open'; defaultStatus = 'open';
status = 'open'; status = 'open';
publicKeySize = 25; publicKeySize = 25;
@ -31,7 +31,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
constructor( constructor(
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
) { ) {
this.channelStatusForm = this.formBuilder.group({ this.channelStatusForm = this.formBuilder.group({
status: [this.defaultStatus], status: [this.defaultStatus],

View File

@ -55,12 +55,12 @@
<div class="toggle-holder"> <div class="toggle-holder">
<form [formGroup]="socketToggleForm" class="formRadioGroup"> <form [formGroup]="socketToggleForm" class="formRadioGroup">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="socket"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="socketToggleForm.get('socket').value === 0">
<input ngbButton type="radio" [value]="0">IPv4 <input type="radio" [value]="0" formControlName="socket">IPv4
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="socketToggleForm.get('socket').value === 1">
<input ngbButton type="radio" [value]="1">IPv6 <input type="radio" [value]="1" formControlName="socket">IPv6
</label> </label>
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { map, Observable, share } from 'rxjs'; import { map, Observable, share } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component'; import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
@ -17,12 +17,12 @@ export class GroupComponent implements OnInit {
skeletonLines: number[] = []; skeletonLines: number[] = [];
selectedSocketIndex = 0; selectedSocketIndex = 0;
qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
socketToggleForm: FormGroup; socketToggleForm: UntypedFormGroup;
constructor( constructor(
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
private seoService: SeoService, private seoService: SeoService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
) { ) {
for (let i = 0; i < 20; ++i) { for (let i = 0; i < 20; ++i) {
this.skeletonLines.push(i); this.skeletonLines.push(i);

View File

@ -12,6 +12,7 @@ import { ChannelsListComponent } from './channels-list/channels-list.component';
import { ChannelComponent } from './channel/channel.component'; import { ChannelComponent } from './channel/channel.component';
import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component'; import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper.component';
import { ChannelBoxComponent } from './channel/channel-box/channel-box.component'; import { ChannelBoxComponent } from './channel/channel-box/channel-box.component';
import { ChannelCloseBoxComponent } from './channel/channel-close-box/channel-close-box.component';
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
@ -45,6 +46,7 @@ import { GroupComponent } from './group/group.component';
ChannelComponent, ChannelComponent,
LightningWrapperComponent, LightningWrapperComponent,
ChannelBoxComponent, ChannelBoxComponent,
ChannelCloseBoxComponent,
ClosingTypeComponent, ClosingTypeComponent,
LightningStatisticsChartComponent, LightningStatisticsChartComponent,
NodesNetworksChartComponent, NodesNetworksChartComponent,
@ -81,6 +83,7 @@ import { GroupComponent } from './group/group.component';
ChannelComponent, ChannelComponent,
LightningWrapperComponent, LightningWrapperComponent,
ChannelBoxComponent, ChannelBoxComponent,
ChannelCloseBoxComponent,
ClosingTypeComponent, ClosingTypeComponent,
LightningStatisticsChartComponent, LightningStatisticsChartComponent,
NodesNetworksChartComponent, NodesNetworksChartComponent,

View File

@ -3,7 +3,7 @@ import { EChartsOption } from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { download } from '../../shared/graphs.utils'; import { download } from '../../shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service'; import { LightningApiService } from '../lightning-api.service';
@ -29,7 +29,7 @@ export class NodeStatisticsChartComponent implements OnInit {
@Input() widget = false; @Input() widget = false;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {

View File

@ -9,27 +9,27 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(nodesNetworkObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(nodesNetworkObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 30"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 30" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 1M <input type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 90"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 90" [class.active]="radioGroupForm.get('dateSpan').value === '3d'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 3M <input type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 180"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 180" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 6M <input type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 365"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 365" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 1Y <input type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 730"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 730" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 2Y <input type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 1095"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 1095" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> 3Y <input type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"> ALL <input type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -3,7 +3,7 @@ import { EChartsOption, graphic, LineSeriesOption} from 'echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils'; import { download } from '../../shared/graphs.utils';
@ -32,7 +32,7 @@ export class NodesNetworksChartComponent implements OnInit {
@Input() widget = false; @Input() widget = false;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -51,7 +51,7 @@ export class NodesNetworksChartComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private amountShortenerPipe: AmountShortenerPipe, private amountShortenerPipe: AmountShortenerPipe,

View File

@ -9,34 +9,34 @@
</div> </div>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(capacityObservable$ | async) as stats"> <form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(capacityObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" name="radioBasic">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 30"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 30" [class.active]="radioGroupForm.get('dateSpan').value === '1m'">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" <input type="radio" [value]="'1m'" fragment="1m"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 1M [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 1M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 90"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 90" [class.active]="radioGroupForm.get('dateSpan').value === '3m'">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" <input type="radio" [value]="'3m'" fragment="3m"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 3M [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 3M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 180"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 180" [class.active]="radioGroupForm.get('dateSpan').value === '6m'">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" <input type="radio" [value]="'6m'" fragment="6m"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 6M [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 6M
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 365"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 365" [class.active]="radioGroupForm.get('dateSpan').value === '1y'">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" <input type="radio" [value]="'1y'" fragment="1y"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 1Y [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 1Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 730"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 730" [class.active]="radioGroupForm.get('dateSpan').value === '2y'">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" <input type="radio" [value]="'2y'" fragment="2y"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 2Y [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 2Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.days >= 1095"> <label class="btn btn-primary btn-sm" *ngIf="stats.days >= 1095" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" <input type="radio" [value]="'3y'" fragment="3y"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> 3Y [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm"> <label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === 'all'">
<input ngbButton type="radio" [value]="'all'" fragment="all" <input type="radio" [value]="'all'" fragment="all"
[routerLink]="['/graphs/lightning/capacity' | relativeUrl]"> ALL [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" formControlName="dateSpan"> ALL
</label> </label>
</div> </div>
</form> </form>

View File

@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils'; import { download } from '../../shared/graphs.utils';
@ -31,7 +31,7 @@ export class LightningStatisticsChartComponent implements OnInit {
@Input() widget = false; @Input() widget = false;
miningWindowPreference: string; miningWindowPreference: string;
radioGroupForm: FormGroup; radioGroupForm: UntypedFormGroup;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
@ -50,7 +50,7 @@ export class LightningStatisticsChartComponent implements OnInit {
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private amountShortenerPipe: AmountShortenerPipe, private amountShortenerPipe: AmountShortenerPipe,

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