diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts new file mode 100644 index 000000000..77a6e7459 --- /dev/null +++ b/backend/src/api/audit.ts @@ -0,0 +1,118 @@ +import logger from '../logger'; +import { BlockExtended, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; + +const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners + +class Audit { + auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) + : { censored: string[], added: string[], score: number } { + if (!projectedBlocks?.[0]?.transactionIds || !mempool) { + return { censored: [], added: [], score: 0 }; + } + + const matches: string[] = []; // present in both mined block and template + const added: string[] = []; // present in mined block, not in template + const fresh: string[] = []; // missing, but firstSeen within PROPAGATION_MARGIN + const isCensored = {}; // missing, without excuse + const isDisplaced = {}; + let displacedWeight = 0; + + const inBlock = {}; + const inTemplate = {}; + + const now = Math.round((Date.now() / 1000)); + for (const tx of transactions) { + inBlock[tx.txid] = tx; + } + // coinbase is always expected + if (transactions[0]) { + inTemplate[transactions[0].txid] = true; + } + // look for transactions that were expected in the template, but missing from the mined block + for (const txid of projectedBlocks[0].transactionIds) { + if (!inBlock[txid]) { + // tx is recent, may have reached the miner too late for inclusion + if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) { + fresh.push(txid); + } else { + isCensored[txid] = true; + } + displacedWeight += mempool[txid].weight; + } + inTemplate[txid] = true; + } + + displacedWeight += (4000 - transactions[0].weight); + + logger.warn(`${fresh.length} fresh, ${Object.keys(isCensored).length} possibly censored, ${displacedWeight} displaced weight`); + + // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs + // these displaced transactions should occupy the first N weight units of the next projected block + let displacedWeightRemaining = displacedWeight; + let index = 0; + let lastFeeRate = Infinity; + let failures = 0; + while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) { + const txid = projectedBlocks[1].transactionIds[index]; + const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000; + const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate; + if (fits || feeMatches) { + isDisplaced[txid] = true; + if (fits) { + lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize); + } + if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) { + displacedWeightRemaining -= mempool[txid].weight; + } + failures = 0; + } else { + failures++; + } + index++; + } + + // mark unexpected transactions in the mined block as 'added' + let overflowWeight = 0; + for (const tx of transactions) { + if (inTemplate[tx.txid]) { + matches.push(tx.txid); + } else { + if (!isDisplaced[tx.txid]) { + added.push(tx.txid); + } + overflowWeight += tx.weight; + } + } + + // transactions missing from near the end of our template are probably not being censored + let overflowWeightRemaining = overflowWeight; + let lastOverflowRate = 1.00; + index = projectedBlocks[0].transactionIds.length - 1; + while (index >= 0) { + const txid = projectedBlocks[0].transactionIds[index]; + if (overflowWeightRemaining > 0) { + if (isCensored[txid]) { + delete isCensored[txid]; + } + lastOverflowRate = mempool[txid].effectiveFeePerVsize; + } else if (Math.floor(mempool[txid].effectiveFeePerVsize * 100) <= Math.ceil(lastOverflowRate * 100)) { // tolerance of 0.01 sat/vb + if (isCensored[txid]) { + delete isCensored[txid]; + } + } + overflowWeightRemaining -= (mempool[txid]?.weight || 0); + index--; + } + + const numCensored = Object.keys(isCensored).length; + const score = matches.length > 0 ? (matches.length / (matches.length + numCensored)) : 0; + + return { + censored: Object.keys(isCensored), + added, + score + }; + } +} + +export default new Audit(); \ No newline at end of file diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index d702b4927..f536ce3d5 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -20,6 +20,7 @@ import indexer from '../indexer'; import fiatConversion from './fiat-conversion'; import poolsParser from './pools-parser'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; +import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import mining from './mining/mining'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import PricesRepository from '../repositories/PricesRepository'; @@ -186,14 +187,18 @@ class Blocks { if (!pool) { // We should never have this situation in practise logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` + `Check your "pools" table entries`); - return blockExtended; + } else { + blockExtended.extras.pool = { + id: pool.id, + name: pool.name, + slug: pool.slug, + }; } - blockExtended.extras.pool = { - id: pool.id, - name: pool.name, - slug: pool.slug, - }; + const auditSummary = await BlocksAuditsRepository.$getShortBlockAudit(block.id); + if (auditSummary) { + blockExtended.extras.matchRate = auditSummary.matchRate; + } } return blockExtended; diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 1dc0b9704..ec201de0c 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 40; + private static currentVersion = 41; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -348,6 +348,10 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);'); } + + if (databaseSchemaVersion < 41 && isBitcoin === true) { + await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1'); + } } /** diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index cbd70a34f..d8dceab19 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -129,6 +129,56 @@ class NodesApi { } } + public async $getFeeHistogram(node_public_key: string): Promise { + try { + const inQuery = ` + SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate) + WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0 + WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0 + WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0 + END as bucket, + count(short_id) as count, + sum(capacity) as capacity + FROM ( + SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate, + short_id as short_id, + capacity as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + ) as fee_rate_table + GROUP BY bucket; + `; + const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]); + + const outQuery = ` + SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate) + WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0 + WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0 + WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0 + END as bucket, + count(short_id) as count, + sum(capacity) as capacity + FROM ( + SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate, + short_id as short_id, + capacity as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + ) as fee_rate_table + GROUP BY bucket; + `; + const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]); + + return { + incoming: inRows.length > 0 ? inRows : [], + outgoing: outRows.length > 0 ? outRows : [], + }; + } catch (e) { + logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`); + throw e; + } + } + public async $getAllNodes(): Promise { try { const query = `SELECT * FROM nodes`; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index cf3f75208..c19bde236 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -20,7 +20,9 @@ class NodesRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup) ; } @@ -33,6 +35,39 @@ class NodesRoutes { } } + private async $getNodeGroup(req: Request, res: Response) { + try { + let nodesList; + let nodes: any[] = []; + switch (config.MEMPOOL.NETWORK) { + case 'testnet': + nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584']; + break; + case 'signet': + nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7']; + break; + default: + nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43']; + } + + for (let pubKey of nodesList) { + try { + const node = await nodesApi.$getNode(pubKey); + if (node) { + nodes.push(node); + } + } catch (e) {} + } + + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(nodes); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getNode(req: Request, res: Response) { try { const node = await nodesApi.$getNode(req.params.public_key); @@ -61,6 +96,22 @@ class NodesRoutes { } } + private async $getFeeHistogram(req: Request, res: Response) { + try { + const node = await nodesApi.$getFeeHistogram(req.params.public_key); + if (!node) { + res.status(404).send('Node not found'); + return; + } + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(node); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getNodesRanking(req: Request, res: Response): Promise { try { const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false); diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index 558ee86fd..cab8bfc29 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -6,7 +6,8 @@ class StatisticsApi { public async $getStatistics(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); - let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, tor_nodes, clearnet_nodes, unannounced_nodes + let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, + tor_nodes, clearnet_nodes, unannounced_nodes, clearnet_tor_nodes FROM lightning_stats`; if (interval) { diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 121fb20ea..9b3c62f04 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -70,6 +70,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`); loggerTimer = new Date().getTime() / 1000; } + + channelProcessed++; } return consolidatedChannelList; diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 5eb5aa9c8..d0c2a4f63 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,7 +1,8 @@ import logger from '../logger'; -import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces'; +import { MempoolBlock, TransactionExtended, AuditTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces'; import { Common } from './common'; import config from '../config'; +import { PairingHeap } from '../utils/pairing-heap'; class MempoolBlocks { private mempoolBlocks: MempoolBlockWithTransactions[] = []; @@ -72,6 +73,7 @@ class MempoolBlocks { logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds'); const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks); + this.mempoolBlocks = blocks; this.mempoolBlockDeltas = deltas; } @@ -99,6 +101,7 @@ class MempoolBlocks { if (transactions.length) { mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length)); } + // Calculate change from previous block states for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { let added: TransactionStripped[] = []; @@ -132,12 +135,286 @@ class MempoolBlocks { removed }); } + return { blocks: mempoolBlocks, deltas: mempoolBlockDeltas }; } + /* + * Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core + * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp) + * + * blockLimit: number of blocks to build in total. + * weightLimit: maximum weight of transactions to consider using the selection algorithm. + * if weightLimit is significantly lower than the mempool size, results may start to diverge from getBlockTemplate + * condenseRest: whether to ignore excess transactions or append them to the final block. + */ + public makeBlockTemplates(mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): MempoolBlockWithTransactions[] { + const start = Date.now(); + const auditPool: { [txid: string]: AuditTransaction } = {}; + const mempoolArray: AuditTransaction[] = []; + const restOfArray: TransactionExtended[] = []; + + let weight = 0; + const maxWeight = weightLimit ? Math.max(4_000_000 * blockLimit, weightLimit) : Infinity; + // grab the top feerate txs up to maxWeight + Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => { + weight += tx.weight; + if (weight >= maxWeight) { + restOfArray.push(tx); + return; + } + // initializing everything up front helps V8 optimize property access later + auditPool[tx.txid] = { + txid: tx.txid, + fee: tx.fee, + size: tx.size, + weight: tx.weight, + feePerVsize: tx.feePerVsize, + vin: tx.vin, + relativesSet: false, + ancestorMap: new Map(), + children: new Set(), + ancestorFee: 0, + ancestorWeight: 0, + score: 0, + used: false, + modified: false, + modifiedNode: null, + } + mempoolArray.push(auditPool[tx.txid]); + }) + + // Build relatives graph & calculate ancestor scores + for (const tx of mempoolArray) { + if (!tx.relativesSet) { + this.setRelatives(tx, auditPool); + } + } + + // Sort by descending ancestor score + mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0)); + + // Build blocks by greedily choosing the highest feerate package + // (i.e. the package rooted in the transaction with the best ancestor score) + const blocks: MempoolBlockWithTransactions[] = []; + let blockWeight = 4000; + let blockSize = 0; + let transactions: AuditTransaction[] = []; + const modified: PairingHeap = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0)); + let overflow: AuditTransaction[] = []; + let failures = 0; + let top = 0; + while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) { + // skip invalid transactions + while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) { + top++; + } + + // Select best next package + let nextTx; + const nextPoolTx = mempoolArray[top]; + const nextModifiedTx = modified.peek(); + if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) { + nextTx = nextPoolTx; + top++; + } else { + modified.pop(); + if (nextModifiedTx) { + nextTx = nextModifiedTx; + nextTx.modifiedNode = undefined; + } + } + + if (nextTx && !nextTx?.used) { + // Check if the package fits into this block + if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) { + blockWeight += nextTx.ancestorWeight; + const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values()); + // 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 effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); + sortedTxSet.forEach((ancestor, i, arr) => { + const mempoolTx = mempool[ancestor.txid]; + if (ancestor && !ancestor?.used) { + ancestor.used = true; + // update original copy of this tx with effective fee rate & relatives data + mempoolTx.effectiveFeePerVsize = effectiveFeeRate; + mempoolTx.ancestors = (Array.from(ancestor.ancestorMap?.values()) as AuditTransaction[]).map((a) => { + return { + txid: a.txid, + fee: a.fee, + weight: a.weight, + } + }) + 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, + }; + } + transactions.push(ancestor); + blockSize += ancestor.size; + } + }); + + // remove these as valid package ancestors for any descendants remaining in the mempool + if (sortedTxSet.length) { + sortedTxSet.forEach(tx => { + this.updateDescendants(tx, auditPool, modified); + }); + } + + failures = 0; + } else { + // hold this package in an overflow list while we check for smaller options + overflow.push(nextTx); + failures++; + } + } + + // this block is full + const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000); + if (exceededPackageTries && (!condenseRest || blocks.length < blockLimit - 1)) { + // construct this block + if (transactions.length) { + blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length)); + } + // reset for the next block + transactions = []; + blockSize = 0; + blockWeight = 4000; + + // 'overflow' packages didn't fit in this block, but are valid candidates for the next + for (const overflowTx of overflow.reverse()) { + if (overflowTx.modified) { + overflowTx.modifiedNode = modified.add(overflowTx); + } else { + top--; + mempoolArray[top] = overflowTx; + } + } + overflow = []; + } + } + if (condenseRest) { + // pack any leftover transactions into the last block + for (const tx of overflow) { + if (!tx || tx?.used) { + continue; + } + blockWeight += tx.weight; + blockSize += tx.size; + transactions.push(tx); + tx.used = true; + } + const blockTransactions = transactions.map(t => mempool[t.txid]) + restOfArray.forEach(tx => { + blockWeight += tx.weight; + blockSize += tx.size; + blockTransactions.push(tx); + }); + if (blockTransactions.length) { + blocks.push(this.dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length)); + } + transactions = []; + } else if (transactions.length) { + blocks.push(this.dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length)); + } + + const end = Date.now(); + const time = end - start; + logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds'); + + return blocks; + } + + // traverse in-mempool ancestors + // recursion unavoidable, but should be limited to depth < 25 by mempool policy + public setRelatives( + tx: AuditTransaction, + mempool: { [txid: string]: AuditTransaction }, + ): void { + for (const parent of tx.vin) { + const parentTx = mempool[parent.txid]; + if (parentTx && !tx.ancestorMap!.has(parent.txid)) { + tx.ancestorMap.set(parent.txid, parentTx); + parentTx.children.add(tx); + // visit each node only once + if (!parentTx.relativesSet) { + this.setRelatives(parentTx, mempool); + } + parentTx.ancestorMap.forEach((ancestor) => { + tx.ancestorMap.set(ancestor.txid, ancestor); + }); + } + }; + tx.ancestorFee = tx.fee || 0; + tx.ancestorWeight = tx.weight || 0; + tx.ancestorMap.forEach((ancestor) => { + tx.ancestorFee += ancestor.fee; + tx.ancestorWeight += ancestor.weight; + }); + tx.score = tx.ancestorFee / (tx.ancestorWeight || 1); + tx.relativesSet = true; + } + + // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score + // avoids recursion to limit call stack depth + private updateDescendants( + rootTx: AuditTransaction, + mempool: { [txid: string]: AuditTransaction }, + modified: PairingHeap, + ): void { + const descendantSet: Set = new Set(); + // stack of nodes left to visit + const descendants: AuditTransaction[] = []; + let descendantTx; + let ancestorIndex; + let tmpScore; + rootTx.children.forEach(childTx => { + if (!descendantSet.has(childTx)) { + descendants.push(childTx); + descendantSet.add(childTx); + } + }); + while (descendants.length) { + descendantTx = descendants.pop(); + if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { + // remove tx as ancestor + descendantTx.ancestorMap.delete(rootTx.txid); + descendantTx.ancestorFee -= rootTx.fee; + descendantTx.ancestorWeight -= rootTx.weight; + tmpScore = descendantTx.score; + descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorWeight; + + if (!descendantTx.modifiedNode) { + descendantTx.modified = true; + descendantTx.modifiedNode = modified.add(descendantTx); + } else { + // rebalance modified heap if score has changed + if (descendantTx.score < tmpScore) { + modified.decreasePriority(descendantTx.modifiedNode); + } else if (descendantTx.score > tmpScore) { + modified.increasePriority(descendantTx.modifiedNode); + } + } + + // add this node's children to the stack + descendantTx.children.forEach(childTx => { + // visit each node only once + if (!descendantSet.has(childTx)) { + descendants.push(childTx); + descendantSet.add(childTx); + } + }); + } + } + } + private dataToMempoolBlocks(transactions: TransactionExtended[], blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions { let rangeLength = 4; diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index ac4b82363..591af3f90 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -239,6 +239,12 @@ class MiningRoutes { public async $getBlockAudit(req: Request, res: Response) { try { const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); + + if (!audit) { + res.status(404).send(`This block has not been audited.`); + return; + } + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 4896ee058..4bd7cfc8d 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -18,6 +18,7 @@ import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; +import Audit from './audit'; class WebsocketHandler { private wss: WebSocket.Server | undefined; @@ -405,75 +406,63 @@ class WebsocketHandler { }); } - handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) { + handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]): void { if (!this.wss) { throw new Error('WebSocket.Server is not set'); } let mBlocks: undefined | MempoolBlock[]; let mBlockDeltas: undefined | MempoolBlockDelta[]; - let matchRate = 0; + let matchRate; const _memPool = memPool.getMempool(); - const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); - if (_mempoolBlocks[0]) { - const matches: string[] = []; - const added: string[] = []; - const missing: string[] = []; + if (Common.indexingEnabled()) { + const mempoolCopy = cloneMempool(_memPool); + const projectedBlocks = mempoolBlocks.makeBlockTemplates(mempoolCopy, 2); - for (const txId of txIds) { - if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) { - matches.push(txId); - } else { - added.push(txId); + const { censored, added, score } = Audit.auditBlock(transactions, projectedBlocks, mempoolCopy); + matchRate = Math.round(score * 100 * 100) / 100; + + const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { + return { + txid: tx.txid, + vsize: tx.vsize, + fee: tx.fee ? Math.round(tx.fee) : 0, + value: tx.value, + }; + }) : []; + + BlocksSummariesRepository.$saveSummary({ + height: block.height, + template: { + id: block.id, + transactions: stripped } - delete _memPool[txId]; - } + }); - for (const txId of _mempoolBlocks[0].transactionIds) { - if (matches.includes(txId) || added.includes(txId)) { - continue; - } - missing.push(txId); - } + BlocksAuditsRepository.$saveAudit({ + time: block.timestamp, + height: block.height, + hash: block.id, + addedTxs: added, + missingTxs: censored, + matchRate: matchRate, + }); - matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100; - mempoolBlocks.updateMempoolBlocks(_memPool); - mBlocks = mempoolBlocks.getMempoolBlocks(); - mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); - - if (Common.indexingEnabled()) { - const stripped = _mempoolBlocks[0].transactions.map((tx) => { - return { - txid: tx.txid, - vsize: tx.vsize, - fee: tx.fee ? Math.round(tx.fee) : 0, - value: tx.value, - }; - }); - BlocksSummariesRepository.$saveSummary({ - height: block.height, - template: { - id: block.id, - transactions: stripped - } - }); - - BlocksAuditsRepository.$saveAudit({ - time: block.timestamp, - height: block.height, - hash: block.id, - addedTxs: added, - missingTxs: missing, - matchRate: matchRate, - }); + if (block.extras) { + block.extras.matchRate = matchRate; } } - if (block.extras) { - block.extras.matchRate = matchRate; + // Update mempool to remove transactions included in the new block + for (const txId of txIds) { + delete _memPool[txId]; } + mempoolBlocks.updateMempoolBlocks(_memPool); + mBlocks = mempoolBlocks.getMempoolBlocks(); + mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); + const da = difficultyAdjustment.getDifficultyAdjustment(); const fees = feeApi.getRecommendedFee(); @@ -580,4 +569,14 @@ class WebsocketHandler { } } +function cloneMempool(mempool: { [txid: string]: TransactionExtended }): { [txid: string]: TransactionExtended } { + const cloned = {}; + Object.keys(mempool).forEach(id => { + cloned[id] = { + ...mempool[id] + }; + }); + return cloned; +} + export default new WebsocketHandler(); diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index d72b13576..32d87f3dc 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -1,4 +1,5 @@ import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; +import { HeapNode } from "./utils/pairing-heap"; export interface PoolTag { id: number; // mysql row id @@ -70,12 +71,40 @@ export interface TransactionExtended extends IEsploraApi.Transaction { deleteAfter?: number; } -interface Ancestor { +export interface AuditTransaction { + txid: string; + fee: number; + size: number; + weight: number; + feePerVsize: number; + vin: IEsploraApi.Vin[]; + relativesSet: boolean; + ancestorMap: Map; + children: Set; + ancestorFee: number; + ancestorWeight: number; + score: number; + used: boolean; + modified: boolean; + modifiedNode: HeapNode; +} + +export interface Ancestor { txid: string; weight: number; fee: number; } +export interface TransactionSet { + fee: number; + weight: number; + score: number; + children?: Set; + available?: boolean; + modified?: boolean; + modifiedNode?: HeapNode; +} + interface BestDescendant { txid: string; weight: number; diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index be85b22b9..188cf4c38 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -58,10 +58,12 @@ class BlocksAuditRepositories { WHERE blocks_audits.hash = "${hash}" `); - rows[0].missingTxs = JSON.parse(rows[0].missingTxs); - rows[0].addedTxs = JSON.parse(rows[0].addedTxs); - rows[0].transactions = JSON.parse(rows[0].transactions); - rows[0].template = JSON.parse(rows[0].template); + if (rows.length) { + rows[0].missingTxs = JSON.parse(rows[0].missingTxs); + rows[0].addedTxs = JSON.parse(rows[0].addedTxs); + rows[0].transactions = JSON.parse(rows[0].transactions); + rows[0].template = JSON.parse(rows[0].template); + } return rows[0]; } catch (e: any) { @@ -69,6 +71,20 @@ class BlocksAuditRepositories { throw e; } } + + public async $getShortBlockAudit(hash: string): Promise { + try { + const [rows]: any[] = await DB.query( + `SELECT hash as id, match_rate as matchRate + FROM blocks_audits + WHERE blocks_audits.hash = "${hash}" + `); + return rows[0]; + } catch (e: any) { + logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } } export default new BlocksAuditRepositories(); diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index 8d8f1759f..838170a3e 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -289,6 +289,24 @@ class NetworkSyncService { 1. Mutually closed 2. Forced closed 3. Forced closed with penalty + + ┌────────────────────────────────────┐ ┌────────────────────────────┐ + │ outputs contain revocation script? ├──yes──► force close w/ penalty = 3 │ + └──────────────┬─────────────────────┘ └────────────────────────────┘ + no + ┌──────────────▼──────────────────────────┐ + │ outputs contain other lightning script? ├──┐ + └──────────────┬──────────────────────────┘ │ + no yes + ┌──────────────▼─────────────┐ │ + │ sequence starts with 0x80 │ ┌────────▼────────┐ + │ and ├──────► force close = 2 │ + │ locktime starts with 0x20? │ └─────────────────┘ + └──────────────┬─────────────┘ + no + ┌─────────▼────────┐ + │ mutual close = 1 │ + └──────────────────┘ */ private async $runClosedChannelsForensics(): Promise { @@ -326,36 +344,31 @@ class NetworkSyncService { lightningScriptReasons.push(lightningScript); } } - if (lightningScriptReasons.length === outspends.length - && lightningScriptReasons.filter((r) => r === 1).length === outspends.length) { - reason = 1; - } else { - const filteredReasons = lightningScriptReasons.filter((r) => r !== 1); - if (filteredReasons.length) { - if (filteredReasons.some((r) => r === 2 || r === 4)) { - reason = 3; - } else { - reason = 2; - } + const filteredReasons = lightningScriptReasons.filter((r) => r !== 1); + if (filteredReasons.length) { + if (filteredReasons.some((r) => r === 2 || r === 4)) { + reason = 3; } else { - /* - 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 - */ - let closingTx: IEsploraApi.Transaction | undefined; - try { - closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id); - } 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; - } - const sequenceHex: string = closingTx.vin[0].sequence.toString(16); - const locktimeHex: string = closingTx.locktime.toString(16); - if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') { - reason = 2; // Here we can't be sure if it's a penalty or not - } else { - reason = 1; - } + reason = 2; + } + } else { + /* + 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 + */ + let closingTx: IEsploraApi.Transaction | undefined; + try { + closingTx = await bitcoinApi.$getRawTransaction(channel.closing_transaction_id); + } 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; + } + const sequenceHex: string = closingTx.vin[0].sequence.toString(16); + const locktimeHex: string = closingTx.locktime.toString(16); + if (sequenceHex.substring(0, 2) === '80' && locktimeHex.substring(0, 2) === '20') { + reason = 2; // Here we can't be sure if it's a penalty or not + } else { + reason = 1; } } if (reason) { diff --git a/backend/src/tasks/lightning/sync-tasks/node-locations.ts b/backend/src/tasks/lightning/sync-tasks/node-locations.ts index 9069e0fff..ba59e9e48 100644 --- a/backend/src/tasks/lightning/sync-tasks/node-locations.ts +++ b/backend/src/tasks/lightning/sync-tasks/node-locations.ts @@ -4,11 +4,14 @@ import nodesApi from '../../../api/explorer/nodes.api'; import config from '../../../config'; import DB from '../../../database'; import logger from '../../../logger'; +import { ResultSetHeader } from 'mysql2'; import * as IPCheck from '../../../utils/ipcheck.js'; export async function $lookupNodeLocation(): Promise { let loggerTimer = new Date().getTime() / 1000; let progress = 0; + let nodesUpdated = 0; + let geoNamesInserted = 0; logger.info(`Running node location updater using Maxmind`); try { @@ -71,51 +74,72 @@ export async function $lookupNodeLocation(): Promise { city.location?.accuracy_radius, node.public_key ]; - await DB.query(query, params); + let result = await DB.query(query, params); + if (result[0].changedRows ?? 0 > 0) { + ++nodesUpdated; + } // Store Continent if (city.continent?.geoname_id) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'continent', ?)`, [city.continent?.geoname_id, JSON.stringify(city.continent?.names)]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } // Store Country if (city.country?.geoname_id) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country', ?)`, [city.country?.geoname_id, JSON.stringify(city.country?.names)]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } // Store Country ISO code if (city.country?.iso_code) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country_iso_code', ?)`, [city.country?.geoname_id, city.country?.iso_code]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } // Store Division if (city.subdivisions && city.subdivisions[0]) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'division', ?)`, [city.subdivisions[0].geoname_id, JSON.stringify(city.subdivisions[0]?.names)]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } // Store City if (city.city?.geoname_id) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'city', ?)`, [city.city?.geoname_id, JSON.stringify(city.city?.names)]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } // Store AS name if (isp?.autonomous_system_organization ?? asn?.autonomous_system_organization) { - await DB.query( + result = await DB.query( `INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'as_organization', ?)`, [ asOverwrite?.asn ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(asOverwrite?.name ?? isp?.isp ?? asn?.autonomous_system_organization) ]); + if (result[0].changedRows ?? 0 > 0) { + ++geoNamesInserted; + } } } @@ -128,7 +152,12 @@ export async function $lookupNodeLocation(): Promise { } } } - logger.info(`${progress} nodes location data updated`); + + if (nodesUpdated > 0) { + logger.info(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`); + } else { + logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`); + } } catch (e) { logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e)); } diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index e3dfe6652..6d6c9e4d3 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -360,9 +360,11 @@ class LightningStatsImporter { fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); } catch (e: any) { if (e.errno == -1) { // EISDIR - Ignore directorie + totalProcessed++; continue; } logger.err(`Unable to open ${this.topologiesFolder}/${filename}`); + totalProcessed++; continue; } @@ -372,6 +374,7 @@ class LightningStatsImporter { graph = await this.cleanupTopology(graph); } catch (e) { logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`); + totalProcessed++; continue; } diff --git a/backend/src/utils/pairing-heap.ts b/backend/src/utils/pairing-heap.ts new file mode 100644 index 000000000..876e056c4 --- /dev/null +++ b/backend/src/utils/pairing-heap.ts @@ -0,0 +1,174 @@ +export type HeapNode = { + element: T + child?: HeapNode + next?: HeapNode + prev?: HeapNode +} | null | undefined; + +// minimal pairing heap priority queue implementation +export class PairingHeap { + private root: HeapNode = null; + private comparator: (a: T, b: T) => boolean; + + // comparator function should return 'true' if a is higher priority than b + constructor(comparator: (a: T, b: T) => boolean) { + this.comparator = comparator; + } + + isEmpty(): boolean { + return !this.root; + } + + add(element: T): HeapNode { + const node: HeapNode = { + element + }; + + this.root = this.meld(this.root, node); + + return node; + } + + // returns the top priority element without modifying the queue + peek(): T | void { + return this.root?.element; + } + + // removes and returns the top priority element + pop(): T | void { + let element; + if (this.root) { + const node = this.root; + element = node.element; + this.root = this.mergePairs(node.child); + } + return element; + } + + deleteNode(node: HeapNode): void { + if (!node) { + return; + } + + if (node === this.root) { + this.root = this.mergePairs(node.child); + } + else { + if (node.prev) { + if (node.prev.child === node) { + node.prev.child = node.next; + } + else { + node.prev.next = node.next; + } + } + if (node.next) { + node.next.prev = node.prev; + } + this.root = this.meld(this.root, this.mergePairs(node.child)); + } + + node.child = null; + node.prev = null; + node.next = null; + } + + // fix the heap after increasing the priority of a given node + increasePriority(node: HeapNode): void { + // already the top priority element + if (!node || node === this.root) { + return; + } + // extract from siblings + if (node.prev) { + if (node.prev?.child === node) { + if (this.comparator(node.prev.element, node.element)) { + // already in a valid position + return; + } + node.prev.child = node.next; + } + else { + node.prev.next = node.next; + } + } + if (node.next) { + node.next.prev = node.prev; + } + + this.root = this.meld(this.root, node); + } + + decreasePriority(node: HeapNode): void { + this.deleteNode(node); + this.root = this.meld(this.root, node); + } + + meld(a: HeapNode, b: HeapNode): HeapNode { + if (!a) { + return b; + } + if (!b || a === b) { + return a; + } + + let parent: HeapNode = b; + let child: HeapNode = a; + if (this.comparator(a.element, b.element)) { + parent = a; + child = b; + } + + child.next = parent.child; + if (parent.child) { + parent.child.prev = child; + } + child.prev = parent; + parent.child = child; + + parent.next = null; + parent.prev = null; + + return parent; + } + + mergePairs(node: HeapNode): HeapNode { + if (!node) { + return null; + } + + let current: HeapNode = node; + let next: HeapNode; + let nextCurrent: HeapNode; + let pairs: HeapNode; + let melded: HeapNode; + while (current) { + next = current.next; + if (next) { + nextCurrent = next.next; + melded = this.meld(current, next); + if (melded) { + melded.prev = pairs; + } + pairs = melded; + } + else { + nextCurrent = null; + current.prev = pairs; + pairs = current; + break; + } + current = nextCurrent; + } + + melded = null; + let prev: HeapNode; + while (pairs) { + prev = pairs.prev; + melded = this.meld(melded, pairs); + pairs = prev; + } + + return melded; + } +} \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index ec5b6921b..df0d8b310 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -113,7 +113,7 @@ https://www.transifex.com/mempool/mempool/dashboard/ * French @Bayernatoor * Korean @kcalvinalvinn * Italian @HodlBits -* Hebrew @Sh0ham +* Hebrew @rapidlab309 * Georgian @wyd_idk * Hungarian @btcdragonlord * Dutch @m__btc diff --git a/frontend/package.json b/frontend/package.json index b5055a5de..c5a062e01 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "scripts": { "ng": "./node_modules/@angular/cli/bin/ng.js", "tsc": "./node_modules/typescript/bin/tsc", - "i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng extract-i18n --out-file ./src/locale/messages.xlf", + "i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf", "i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force", "serve": "npm run generate-config && npm run ng -- serve -c local", "serve:stg": "npm run generate-config && npm run ng -- serve -c staging", @@ -34,8 +34,8 @@ "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging", "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js", - "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js", - "sync-assets-dev": "node sync-assets.js dev", + "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", + "sync-assets-dev": "node sync-assets.js 'src/resources/'", "generate-config": "node generate-config.js", "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js", "build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js", diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js index 098edb619..0cf366ca7 100644 --- a/frontend/proxy.conf.staging.js +++ b/frontend/proxy.conf.staging.js @@ -3,9 +3,9 @@ const fs = require('fs'); let PROXY_CONFIG = require('./proxy.conf'); PROXY_CONFIG.forEach(entry => { - entry.target = entry.target.replace("mempool.space", "mempool.ninja"); - entry.target = entry.target.replace("liquid.network", "liquid.place"); - entry.target = entry.target.replace("bisq.markets", "bisq.ninja"); + entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); + entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); + entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space"); }); module.exports = PROXY_CONFIG; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 260efcafe..69c78fc83 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -74,12 +74,14 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -90,6 +92,7 @@ let routes: Routes = [ { path: 'block', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -102,6 +105,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -121,12 +125,13 @@ let routes: Routes = [ { path: 'lightning', loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), - data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true }, + data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -185,11 +190,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -200,6 +207,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -213,6 +221,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -230,12 +239,14 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -291,11 +302,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -306,6 +319,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -319,6 +333,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -336,6 +351,7 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], @@ -359,6 +375,7 @@ let routes: Routes = [ }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'featured', + data: { networkSpecific: true }, component: AssetsFeaturedComponent, }, { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid']}, component: StatusViewComponent }, { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 5ae0c6cb5..6ed7c43f9 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppRoutingModule } from './app-routing.module'; @@ -20,6 +20,23 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; import { AppPreloadingStrategy } from './app.preloading-strategy'; +const providers = [ + ElectrsApiService, + StateService, + WebsocketService, + AudioService, + SeoService, + OpenGraphService, + StorageService, + EnterpriseService, + LanguageService, + ShortenStringPipe, + FiatShortenerPipe, + CapAddressPipe, + AppPreloadingStrategy, + { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } +]; + @NgModule({ declarations: [ AppComponent, @@ -32,22 +49,17 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; BrowserAnimationsModule, SharedModule, ], - providers: [ - ElectrsApiService, - StateService, - WebsocketService, - AudioService, - SeoService, - OpenGraphService, - StorageService, - EnterpriseService, - LanguageService, - ShortenStringPipe, - FiatShortenerPipe, - CapAddressPipe, - AppPreloadingStrategy, - { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } - ], + providers: providers, bootstrap: [AppComponent] }) export class AppModule { } + +@NgModule({}) +export class MempoolSharedModule{ + static forRoot(): ModuleWithProviders { + return { + ngModule: AppModule, + providers: providers + }; + } +} diff --git a/frontend/src/app/bisq/bisq-address/bisq-address.component.ts b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts index 75225d9de..eccc88bc7 100644 --- a/frontend/src/app/bisq/bisq-address/bisq-address.component.ts +++ b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { switchMap, filter, catchError } from 'rxjs/operators'; import { ParamMap, ActivatedRoute } from '@angular/router'; import { Subscription, of } from 'rxjs'; import { BisqTransaction } from '../bisq.interfaces'; import { BisqApiService } from '../bisq-api.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-address', diff --git a/frontend/src/app/bisq/bisq-block/bisq-block.component.ts b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts index 2510ee67f..1bb3a24ab 100644 --- a/frontend/src/app/bisq/bisq-block/bisq-block.component.ts +++ b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { BisqBlock } from 'src/app/bisq/bisq.interfaces'; +import { BisqBlock } from '../../bisq/bisq.interfaces'; import { Location } from '@angular/common'; import { BisqApiService } from '../bisq-api.service'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Subscription, of } from 'rxjs'; import { switchMap, catchError } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; +import { ElectrsApiService } from '../../services/electrs-api.service'; import { HttpErrorResponse } from '@angular/common/http'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-block', diff --git a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts index 1e805188b..8d9ed3c11 100644 --- a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts +++ b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts @@ -3,9 +3,9 @@ import { BisqApiService } from '../bisq-api.service'; import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-blocks', diff --git a/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts index 402c9b6fb..fe36f1b53 100644 --- a/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts +++ b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs'; import { map, share, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts b/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts index a45bb138e..d1b8480f7 100644 --- a/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts +++ b/frontend/src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs'; import { map, share, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-market/bisq-market.component.ts b/frontend/src/app/bisq/bisq-market/bisq-market.component.ts index 90832122f..fb5967c63 100644 --- a/frontend/src/app/bisq/bisq-market/bisq-market.component.ts +++ b/frontend/src/app/bisq/bisq-market/bisq-market.component.ts @@ -3,8 +3,8 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { combineLatest, merge, Observable, of } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; import { BisqApiService } from '../bisq-api.service'; import { OffersMarket, Trade } from '../bisq.interfaces'; diff --git a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts index 11064b5fe..5ec5964b4 100644 --- a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts +++ b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { BisqApiService } from '../bisq-api.service'; import { BisqStats } from '../bisq.interfaces'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-stats', diff --git a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts index d10d0507e..9728372c3 100644 --- a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts +++ b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts @@ -1,5 +1,5 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; @Component({ selector: 'app-bisq-transaction-details', diff --git a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts index c736a2fa9..fb30fc59f 100644 --- a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; import { switchMap, map, catchError } from 'rxjs/operators'; import { of, Observable, Subscription } from 'rxjs'; -import { StateService } from 'src/app/services/state.service'; -import { Block, Transaction } from 'src/app/interfaces/electrs.interface'; +import { StateService } from '../../services/state.service'; +import { Block, Transaction } from '../../interfaces/electrs.interface'; import { BisqApiService } from '../bisq-api.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { SeoService } from '../../services/seo.service'; +import { ElectrsApiService } from '../../services/electrs-api.service'; import { HttpErrorResponse } from '@angular/common/http'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-transaction', diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts index d0a2ba3c5..9c58577e3 100644 --- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts @@ -4,11 +4,11 @@ import { BisqTransaction, BisqOutput } from '../bisq.interfaces'; import { Observable, Subscription } from 'rxjs'; import { switchMap, map, tap } from 'rxjs/operators'; import { BisqApiService } from '../bisq-api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; import { FormGroup, FormBuilder } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; -import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types' -import { WebsocketService } from 'src/app/services/websocket.service'; +import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types' +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-bisq-transactions', diff --git a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts index 2fa805c61..4346f15d3 100644 --- a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts +++ b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; -import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; -import { StateService } from 'src/app/services/state.service'; +import { BisqTransaction } from '../../bisq/bisq.interfaces'; +import { StateService } from '../../services/state.service'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; -import { Block } from 'src/app/interfaces/electrs.interface'; +import { Block } from '../../interfaces/electrs.interface'; @Component({ selector: 'app-bisq-transfers', diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts index f7385ae63..11acdca2a 100644 --- a/frontend/src/app/bisq/bisq.routing.module.ts +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -20,14 +20,17 @@ const routes: Routes = [ }, { path: 'markets', + data: { networks: ['bisq'] }, component: BisqDashboardComponent, }, { path: 'transactions', + data: { networks: ['bisq'] }, component: BisqTransactionsComponent }, { path: 'market/:pair', + data: { networkSpecific: true }, component: BisqMarketComponent, }, { @@ -36,6 +39,7 @@ const routes: Routes = [ }, { path: 'tx/:id', + data: { networkSpecific: true }, component: BisqTransactionComponent }, { @@ -45,14 +49,17 @@ const routes: Routes = [ }, { path: 'block/:id', + data: { networkSpecific: true }, component: BisqBlockComponent, }, { path: 'address/:id', + data: { networkSpecific: true }, component: BisqAddressComponent, }, { path: 'stats', + data: { networks: ['bisq'] }, component: BisqStatsComponent, }, { diff --git a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts index 263b9d7f7..a3dd10e81 100644 --- a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts +++ b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; @Component({ diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 693be2d34..d26efb411 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -1,13 +1,13 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { WebsocketService } from '../../services/websocket.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { StateService } from 'src/app/services/state.service'; +import { SeoService } from '../../services/seo.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; -import { ApiService } from 'src/app/services/api.service'; -import { IBackendInfo } from 'src/app/interfaces/websocket.interface'; +import { ApiService } from '../../services/api.service'; +import { IBackendInfo } from '../../interfaces/websocket.interface'; import { Router } from '@angular/router'; import { map } from 'rxjs/operators'; -import { ITranslators } from 'src/app/interfaces/node-api.interface'; +import { ITranslators } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-about', diff --git a/frontend/src/app/components/address-labels/address-labels.component.html b/frontend/src/app/components/address-labels/address-labels.component.html index 353e733ae..dfc6647f4 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.html +++ b/frontend/src/app/components/address-labels/address-labels.component.html @@ -1,9 +1,16 @@ - - {{ label }} - + +
+ +   +
+
diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts index 52da15125..941d9e21e 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; -import { EnterpriseService } from 'src/app/services/enterprise.service'; +import { LanguageService } from '../../services/language.service'; +import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-bisq-master-page', @@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit { env: Env; isMobile = window.innerWidth <= 767.98; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { this.env = this.stateService.env; this.connectionState$ = this.stateService.connectionState$; this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/block-audit/block-audit.component.html b/frontend/src/app/components/block-audit/block-audit.component.html index 0ee6bef44..543dbb705 100644 --- a/frontend/src/app/components/block-audit/block-audit.component.html +++ b/frontend/src/app/components/block-audit/block-audit.component.html @@ -1,21 +1,22 @@
-
-
-

- - Block -   - {{ blockAudit.height }} -   - Template vs Mined - -

+
+

+ + Block Audit +   + {{ blockAudit.height }} +   + +

-
+
- -
+ +
+ +
+
@@ -26,8 +27,8 @@ Hash - {{ blockAudit.id | shortenString : 13 }} - + {{ blockHash | shortenString : 13 }} + @@ -40,6 +41,10 @@
+ + Transactions + {{ blockAudit.tx_count }} + Size @@ -57,21 +62,25 @@ - - - - - + - + + + + + + + + +
Transactions{{ blockAudit.tx_count }}
Match rateBlock health {{ blockAudit.matchRate }}%
Missing txsRemoved txs {{ blockAudit.missingTxs.length }}
Omitted txs{{ numMissing }}
Added txs {{ blockAudit.addedTxs.length }}
Included txs{{ numUnexpected }}
@@ -79,33 +88,110 @@
- + +
+

+ + Block Audit +   + {{ blockAudit.height }} +   + +

+ +
+ + +
+ + +
+
+ +
+ + + + + + + + +
+
+ + +
+ + + + + + + + +
+
+
+
+ + + +
+ + +
+
+ audit unavailable +

+ {{ error.error }} +
+
+
+ +
+
+ Error loading data. +

+ {{ error }} +
+
+
+
+
+ -
+
- Projected Block +
- Actual Block +
- -
\ No newline at end of file diff --git a/frontend/src/app/components/block-audit/block-audit.component.scss b/frontend/src/app/components/block-audit/block-audit.component.scss index 7ec503891..1e35b7c63 100644 --- a/frontend/src/app/components/block-audit/block-audit.component.scss +++ b/frontend/src/app/components/block-audit/block-audit.component.scss @@ -37,4 +37,8 @@ @media (min-width: 768px) { max-width: 150px; } +} + +.block-subtitle { + text-align: center; } \ No newline at end of file diff --git a/frontend/src/app/components/block-audit/block-audit.component.ts b/frontend/src/app/components/block-audit/block-audit.component.ts index 044552a3b..f8ce8d9bb 100644 --- a/frontend/src/app/components/block-audit/block-audit.component.ts +++ b/frontend/src/app/components/block-audit/block-audit.component.ts @@ -1,12 +1,12 @@ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { Observable } from 'rxjs'; -import { map, share, switchMap, tap } from 'rxjs/operators'; -import { BlockAudit, TransactionStripped } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; -import { detectWebGL } from 'src/app/shared/graphs.utils'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { Subscription, combineLatest } from 'rxjs'; +import { map, switchMap, startWith, catchError } from 'rxjs/operators'; +import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/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({ @@ -22,22 +22,30 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv } `], }) -export class BlockAuditComponent implements OnInit, OnDestroy { +export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy { blockAudit: BlockAudit = undefined; transactions: string[]; - auditObservable$: Observable; + auditSubscription: Subscription; + urlFragmentSubscription: Subscription; paginationMaxSize: number; page = 1; itemsPerPage: number; - mode: 'missing' | 'added' = 'missing'; + mode: 'projected' | 'actual' = 'projected'; + error: any; isLoading = true; webGlEnabled = true; isMobile = window.innerWidth <= 767.98; - @ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent; - @ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent; + childChangeSubscription: Subscription; + + blockHash: string; + numMissing: number = 0; + numUnexpected: number = 0; + + @ViewChildren('blockGraphProjected') blockGraphProjected: QueryList; + @ViewChildren('blockGraphActual') blockGraphActual: QueryList; constructor( private route: ActivatedRoute, @@ -48,73 +56,137 @@ export class BlockAuditComponent implements OnInit, OnDestroy { this.webGlEnabled = detectWebGL(); } - ngOnDestroy(): void { + 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.auditObservable$ = this.route.paramMap.pipe( + 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: string = params.get('id') || ''; - return this.apiService.getBlockAudit$(blockHash) + this.blockHash = params.get('id') || null; + if (!this.blockHash) { + return null; + } + return this.apiService.getBlockAudit$(this.blockHash) .pipe( map((response) => { const blockAudit = response.body; - for (let i = 0; i < blockAudit.template.length; ++i) { - if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) { - blockAudit.template[i].status = 'missing'; - } else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) { - blockAudit.template[i].status = 'added'; + 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 { - blockAudit.template[i].status = 'found'; + tx.status = 'missing'; + isMissing[tx.txid] = true; + this.numMissing++; } } - for (let i = 0; i < blockAudit.transactions.length; ++i) { - if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) { - blockAudit.transactions[i].status = 'missing'; - } else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) { - blockAudit.transactions[i].status = 'added'; + for (const [index, tx] of blockAudit.transactions.entries()) { + if (isAdded[tx.txid]) { + tx.status = 'added'; + } else if (index === 0 || inTemplate[tx.txid]) { + tx.status = 'found'; } else { - blockAudit.transactions[i].status = 'found'; + tx.status = 'selected'; + isSelected[tx.txid] = true; + this.numUnexpected++; } } + for (const tx of blockAudit.transactions) { + inBlock[tx.txid] = true; + } return blockAudit; - }), - tap((blockAudit) => { - this.changeMode(this.mode); - if (this.blockGraphTemplate) { - this.blockGraphTemplate.destroy(); - this.blockGraphTemplate.setup(blockAudit.template); - } - if (this.blockGraphMined) { - this.blockGraphMined.destroy(); - this.blockGraphMined.setup(blockAudit.transactions); - } - this.isLoading = false; - }), + }) ); }), - share() - ); + catchError((err) => { + console.log(err); + this.error = err; + this.isLoading = false; + return 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) { - this.isMobile = event.target.innerWidth <= 767.98; + 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: 'missing' | 'added') { + changeMode(mode: 'projected' | 'actual') { this.router.navigate([], { fragment: mode }); - this.mode = mode; } onTxClick(event: TransactionStripped): void { const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`); this.router.navigate([url]); } - - pageChange(page: number, target: HTMLElement) { - } } diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts index 949024d4c..d05fbdfb9 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -2,16 +2,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, O import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from 'src/app/services/state.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { selectPowerOfTen } from '../../bitcoin.utils'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; import { ActivatedRoute, Router } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts index a64c7e36e..c88e43019 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -2,15 +2,15 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; import { ActivatedRoute } from '@angular/router'; -import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe'; +import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; @Component({ selector: 'app-block-fees-graph', diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index 4cc465ad7..14607f398 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -1,5 +1,5 @@ -import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy } from '@angular/core'; -import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; +import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core'; +import { TransactionStripped } from '../../interfaces/websocket.interface'; import { FastVertexArray } from './fast-vertex-array'; import BlockScene from './block-scene'; import TxSprite from './tx-sprite'; @@ -11,7 +11,7 @@ import { Position } from './sprite-types'; templateUrl: './block-overview-graph.component.html', styleUrls: ['./block-overview-graph.component.scss'], }) -export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy { +export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, OnChanges { @Input() isLoading: boolean; @Input() resolution: number; @Input() blockLimit: number; @@ -57,6 +57,14 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy { this.resizeCanvas(); } + ngOnChanges(changes): void { + if (changes.orientation || changes.flip) { + if (this.scene) { + this.scene.setOrientation(this.orientation, this.flip); + } + } + } + ngOnDestroy(): void { if (this.animationFrameRequest) { cancelAnimationFrame(this.animationFrameRequest); diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index af64c0f20..39ac44e7a 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -1,6 +1,6 @@ import { FastVertexArray } from './fast-vertex-array'; import TxView from './tx-view'; -import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; +import { TransactionStripped } from '../../interfaces/websocket.interface'; import { Position, Square, ViewUpdateParams } from './sprite-types'; export default class BlockScene { @@ -42,6 +42,15 @@ export default class BlockScene { } } + setOrientation(orientation: string, flip: boolean): void { + this.orientation = orientation; + this.flip = flip; + this.dirty = true; + if (this.initialised && this.scene) { + this.updateAll(performance.now(), 50); + } + } + // Destroy the current layout and clean up graphics sprites without any exit animation destroy(): void { Object.values(this.txs).forEach(tx => tx.destroy()); diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index c0b980d5c..ac2a4655a 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -1,12 +1,21 @@ import TxSprite from './tx-sprite'; import { FastVertexArray } from './fast-vertex-array'; -import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; +import { TransactionStripped } from '../../interfaces/websocket.interface'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; -import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; +import { feeLevels, mempoolFeeColors } from '../../app.constants'; const hoverTransitionTime = 300; const defaultHoverColor = hexToColor('1bd8f4'); +const feeColors = mempoolFeeColors.map(hexToColor); +const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3)); +const auditColors = { + censored: hexToColor('f344df'), + missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), + added: hexToColor('03E1E5'), + selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7), +} + // convert from this class's update format to TxSprite's update format function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams { return { @@ -25,7 +34,7 @@ export default class TxView implements TransactionStripped { vsize: number; value: number; feerate: number; - status?: 'found' | 'missing' | 'added'; + status?: 'found' | 'missing' | 'added' | 'censored' | 'selected'; initialised: boolean; vertexArray: FastVertexArray; @@ -142,16 +151,23 @@ export default class TxView implements TransactionStripped { } getColor(): Color { - // Block audit - if (this.status === 'missing') { - return hexToColor('039BE5'); - } else if (this.status === 'added') { - return hexToColor('D81B60'); - } - - // Block component const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1; - return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]); + const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; + // Block audit + switch(this.status) { + case 'censored': + return auditColors.censored; + case 'missing': + return auditColors.missing; + case 'added': + return auditColors.added; + case 'selected': + return auditColors.selected; + case 'found': + return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; + default: + return feeLevelColor; + } } } @@ -163,3 +179,22 @@ function hexToColor(hex: string): Color { a: 1 }; } + +function desaturate(color: Color, amount: number): Color { + const gray = (color.r + color.g + color.b) / 6; + return { + r: color.r + ((gray - color.r) * amount), + g: color.g + ((gray - color.g) * amount), + b: color.b + ((gray - color.b) * amount), + a: color.a, + }; +} + +function darken(color: Color, amount: number): Color { + return { + r: color.r * amount, + g: color.g * amount, + b: color.b * amount, + a: color.a, + } +} diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index 03d7fc1e9..b19b67b06 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -32,6 +32,16 @@ Virtual size + + Audit status + + match + removed + missing + added + included + +
diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts index 603b5fcdb..e30f40b9a 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core'; -import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; -import { Position } from 'src/app/components/block-overview-graph/sprite-types.js'; +import { TransactionStripped } from '../../interfaces/websocket.interface'; +import { Position } from '../../components/block-overview-graph/sprite-types.js'; @Component({ selector: 'app-block-overview-tooltip', diff --git a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts index e88dc07be..289a1e2d3 100644 --- a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts +++ b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts @@ -2,15 +2,15 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, O import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; -import { StorageService } from 'src/app/services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { StateService } from 'src/app/services/state.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-block-prediction-graph', diff --git a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts index 5cd9feea2..cba39eae5 100644 --- a/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts +++ b/frontend/src/app/components/block-rewards-graph/block-rewards-graph.component.ts @@ -2,15 +2,15 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils'; -import { MiningService } from 'src/app/services/mining.service'; -import { StorageService } from 'src/app/services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils'; +import { MiningService } from '../../services/mining.service'; +import { StorageService } from '../../services/storage.service'; import { ActivatedRoute } from '@angular/router'; -import { FiatShortenerPipe } from 'src/app/shared/pipes/fiat-shortener.pipe'; +import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; @Component({ selector: 'app-block-rewards-graph', diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts index cf6591548..6b5b1f047 100644 --- a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts +++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts @@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, H import { EChartsOption} from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; import { ActivatedRoute } from '@angular/router'; -import { download, formatterXAxis } from 'src/app/shared/graphs.utils'; +import { download, formatterXAxis } from '../../shared/graphs.utils'; @Component({ selector: 'app-block-sizes-weights-graph', diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index 6b96887f0..d0fec960a 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -4,11 +4,11 @@ import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; import { of, Subscription, asyncScheduler } from 'rxjs'; import { StateService } from '../../services/state.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { OpenGraphService } from 'src/app/services/opengraph.service'; -import { BlockExtended, TransactionStripped } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; +import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; @Component({ selector: 'app-block-preview', diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index d21cea34e..819b05c81 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -1,36 +1,24 @@
-
+

- Genesis - - - - - {{ blockHeight }} - - - + Block + Genesis + + + + - - Block - - - - - - - - - {{ blockHeight }} - - - - - - + {{ blockHeight }} + + + - +

@@ -122,6 +110,13 @@ + + Block health + + {{ block.extras.matchRate }}% + Unknown + + diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index f047cbcfa..d6c4d65b4 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -111,7 +111,8 @@ h1 { .next-previous-blocks { font-size: 28px; - display: inline-block; + display: inline-flex; + flex-direction: row; @media (min-width: 768px) { font-size: 36px; } @@ -125,6 +126,21 @@ h1 { } } +.time-ltr .next-previous-blocks { + .nav-arrow { + transform: scaleX(-1); + } + .nav-arrow.next { + order: 2; + } + .block-link { + order: 1; + } + .nav-arrow.prev { + order: 0; + } +} + .disable { font-size: 28px; color: #393e5c73; diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 4862e4e5c..8f977b81d 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -6,13 +6,13 @@ import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs'; import { StateService } from '../../services/state.service'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { BlockExtended, TransactionStripped } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; -import { detectWebGL } from 'src/app/shared/graphs.utils'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; +import { detectWebGL } from '../../shared/graphs.utils'; @Component({ selector: 'app-block', @@ -47,6 +47,7 @@ export class BlockComponent implements OnInit, OnDestroy { transactionsError: any = null; overviewError: any = null; webGlEnabled = true; + indexingAvailable = false; transactionSubscription: Subscription; overviewSubscription: Subscription; @@ -57,6 +58,8 @@ export class BlockComponent implements OnInit, OnDestroy { nextBlockSubscription: Subscription = undefined; nextBlockSummarySubscription: Subscription = undefined; nextBlockTxListSubscription: Subscription = undefined; + timeLtrSubscription: Subscription; + timeLtr: boolean; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @@ -80,6 +83,13 @@ export class BlockComponent implements OnInit, OnDestroy { this.network = this.stateService.network; this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE; + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + }); + + this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && + this.stateService.env.MINING_DASHBOARD === true); + this.txsLoadingStatus$ = this.route.paramMap .pipe( switchMap(() => this.stateService.loadingIndicators$), @@ -277,10 +287,12 @@ export class BlockComponent implements OnInit, OnDestroy { }); this.keyNavigationSubscription = this.stateService.keyNavigation$.subscribe((event) => { - if (this.showPreviousBlocklink && event.key === 'ArrowRight' && this.nextBlockHeight - 2 >= 0) { + const prevKey = this.timeLtr ? 'ArrowLeft' : 'ArrowRight'; + const nextKey = this.timeLtr ? 'ArrowRight' : 'ArrowLeft'; + if (this.showPreviousBlocklink && event.key === prevKey && this.nextBlockHeight - 2 >= 0) { this.navigateToPreviousBlock(); } - if (event.key === 'ArrowLeft') { + if (event.key === nextKey) { if (this.showNextBlocklink) { this.navigateToNextBlock(); } else { @@ -298,6 +310,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.networkChangedSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); + this.timeLtrSubscription.unsubscribe(); this.unsubscribeNextBlockSubscriptions(); } @@ -392,8 +405,8 @@ export class BlockComponent implements OnInit, OnDestroy { } setNextAndPreviousBlockLink(){ - if (this.latestBlock && this.blockHeight) { - if (this.blockHeight === 0){ + if (this.latestBlock) { + if (!this.blockHeight){ this.showPreviousBlocklink = false; } else { this.showPreviousBlocklink = true; diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index f1d2cca6d..6bd617435 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -1,4 +1,4 @@ -
+
-
+
diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index 8c108dff7..adde4a945 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -22,7 +22,7 @@ .mined-block { position: absolute; top: 0px; - transition: 2s; + transition: background 2s, left 2s, transform 1s; } .block-size { @@ -145,3 +145,9 @@ opacity: 0; pointer-events : none; } + +.time-ltr { + .bitcoin-block { + transform: scaleX(-1); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 4fd7d7ada..8ac925eaf 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { StateService } from 'src/app/services/state.service'; -import { specialBlocks } from 'src/app/app.constants'; -import { BlockExtended } from 'src/app/interfaces/node-api.interface'; +import { StateService } from '../../services/state.service'; +import { specialBlocks } from '../../app.constants'; +import { BlockExtended } from '../../interfaces/node-api.interface'; import { Location } from '@angular/common'; import { config } from 'process'; @@ -33,6 +33,8 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { blocksFilled = false; transition = '1s'; showMiningInfo = false; + timeLtrSubscription: Subscription; + timeLtr: boolean; gradientColors = { '': ['#9339f4', '#105fb0'], @@ -61,6 +63,11 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url)); } + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + this.cd.markForCheck(); + }); + if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { this.feeRounding = '1.0-1'; } @@ -123,6 +130,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { this.networkSubscription.unsubscribe(); this.tabHiddenSubscription.unsubscribe(); this.markBlockSubscription.unsubscribe(); + this.timeLtrSubscription.unsubscribe(); clearInterval(this.interval); } diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index c49d08c5a..66ae8dd43 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,9 +1,13 @@ -
-
+
+
- - -
+
+ + +
+
+ +
diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 990cf9535..df609ff40 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -16,7 +16,7 @@ .blockchain-wrapper { height: 250px; - -webkit-user-select: none; /* Safari */ + -webkit-user-select: none; /* Safari */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+/Edge */ user-select: none; /* Standard */ @@ -24,23 +24,45 @@ .position-container { position: absolute; - left: 50%; + left: 0; top: 75px; + transform: translateX(50vw); } .position-container.liquid, .position-container.liquidtestnet { - left: 420px; + transform: translateX(420px); +} + +@media (min-width: 768px) { + .blockchain-wrapper.time-ltr { + .position-container.liquid, .position-container.liquidtestnet { + transform: translateX(calc(100vw - 420px)); + } + } } @media (max-width: 767.98px) { - .position-container { - left: 95%; + .blockchain-wrapper { + .position-container { + transform: translateX(95vw); + } + .position-container.liquid, .position-container.liquidtestnet { + transform: translateX(50vw); + } + .position-container.loading { + transform: translateX(50vw); + } } - .position-container.liquid, .position-container.liquidtestnet { - left: 50%; - } - .position-container.loading { - left: 50%; + .blockchain-wrapper.time-ltr { + .position-container { + transform: translateX(5vw); + } + .position-container.liquid, .position-container.liquidtestnet { + transform: translateX(50vw); + } + .position-container.loading { + transform: translateX(50vw); + } } } @@ -57,4 +79,48 @@ width: 300px; left: -150px; top: 0px; +} + +.time-toggle { + color: white; + font-size: 0.8rem; + position: absolute; + bottom: -1.8em; + left: 1px; + transform: translateX(-50%); + background: none; + border: none; + outline: none; + margin: 0; + padding: 0; +} + +.blockchain-wrapper.ltr-transition .blocks-wrapper, +.blockchain-wrapper.ltr-transition .position-container, +.blockchain-wrapper.ltr-transition .time-toggle { + transition: transform 1s; +} + +.blockchain-wrapper.time-ltr { + .blocks-wrapper { + transform: scaleX(-1); + } + + .time-toggle { + transform: translateX(-50%) scaleX(-1); + } +} + +:host-context(.ltr-layout) { + .blockchain-wrapper.time-ltr .blocks-wrapper, + .blockchain-wrapper .blocks-wrapper { + direction: ltr; + } +} + +:host-context(.rtl-layout) { + .blockchain-wrapper.time-ltr .blocks-wrapper, + .blockchain-wrapper .blocks-wrapper { + direction: rtl; + } } \ No newline at end of file diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index 5c00c5ef7..e99b3532d 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -1,5 +1,6 @@ -import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-blockchain', @@ -7,8 +8,11 @@ import { StateService } from 'src/app/services/state.service'; styleUrls: ['./blockchain.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class BlockchainComponent implements OnInit { +export class BlockchainComponent implements OnInit, OnDestroy { network: string; + timeLtrSubscription: Subscription; + timeLtr: boolean = this.stateService.timeLtr.value; + ltrTransitionEnabled = false; constructor( public stateService: StateService, @@ -16,5 +20,17 @@ export class BlockchainComponent implements OnInit { ngOnInit() { this.network = this.stateService.network; + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + }); + } + + ngOnDestroy() { + this.timeLtrSubscription.unsubscribe(); + } + + toggleTimeDirection() { + this.ltrTransitionEnabled = true; + this.stateService.timeLtr.next(!this.timeLtr); } } diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index 660481ecd..68acf71ea 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -14,6 +14,8 @@ i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool Timestamp Mined + Health Reward Fees @@ -37,12 +39,30 @@ {{ block.extras.coinbaseRaw | hex2ascii }}
- + ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + +
+
+
+
+ {{ block.extras.matchRate }}% +
+
+
+
+
+
+ ~ +
+
+ @@ -77,6 +97,9 @@ + + + diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.scss b/frontend/src/app/components/blocks-list/blocks-list.component.scss index 5dc265017..6617cec58 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.scss +++ b/frontend/src/app/components/blocks-list/blocks-list.component.scss @@ -63,7 +63,7 @@ tr, td, th { } .height { - width: 10%; + width: 8%; } .height.widget { width: 15%; @@ -77,12 +77,18 @@ tr, td, th { .timestamp { width: 18%; - @media (max-width: 900px) { + @media (max-width: 1100px) { display: none; } } .timestamp.legacy { width: 20%; + @media (max-width: 1100px) { + display: table-cell; + } + @media (max-width: 850px) { + display: none; + } } .mined { @@ -93,6 +99,10 @@ tr, td, th { } .mined.legacy { width: 15%; + @media (max-width: 1000px) { + padding-right: 20px; + width: 20%; + } @media (max-width: 576px) { display: table-cell; } @@ -100,6 +110,7 @@ tr, td, th { .txs { padding-right: 40px; + width: 8%; @media (max-width: 1100px) { padding-right: 10px; } @@ -113,17 +124,21 @@ tr, td, th { } .txs.widget { padding-right: 0; + display: none; @media (max-width: 650px) { display: none; } } .txs.legacy { - padding-right: 80px; - width: 10%; + width: 18%; + display: table-cell; + @media (max-width: 1000px) { + padding-right: 20px; + } } .fees { - width: 10%; + width: 8%; @media (max-width: 650px) { display: none; } @@ -133,7 +148,7 @@ tr, td, th { } .reward { - width: 10%; + width: 8%; @media (max-width: 576px) { width: 7%; padding-right: 30px; @@ -152,8 +167,11 @@ tr, td, th { } .size { - width: 12%; + width: 10%; @media (max-width: 1000px) { + width: 13%; + } + @media (max-width: 950px) { width: 15%; } @media (max-width: 650px) { @@ -164,12 +182,34 @@ tr, td, th { } } .size.legacy { - width: 20%; + width: 30%; @media (max-width: 576px) { display: table-cell; } } +.health { + width: 10%; + @media (max-width: 1000px) { + width: 13%; + } + @media (max-width: 950px) { + display: none; + } +} +.health.widget { + width: 25%; + @media (max-width: 1000px) { + display: none; + } + @media (max-width: 767px) { + display: table-cell; + } + @media (max-width: 500px) { + display: none; + } +} + /* Tooltip text */ .tooltip-custom { position: relative; diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index 99f95d7b1..7e4c34eb4 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs'; import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { BlockExtended } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-blocks-list', diff --git a/frontend/src/app/components/clipboard/clipboard.component.html b/frontend/src/app/components/clipboard/clipboard.component.html index 1e88b7b7f..ec8802634 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.html +++ b/frontend/src/app/components/clipboard/clipboard.component.html @@ -1,5 +1,15 @@ - - - + + + + + + + diff --git a/frontend/src/app/components/clipboard/clipboard.component.ts b/frontend/src/app/components/clipboard/clipboard.component.ts index e6a5c0e6e..7fbffdca3 100644 --- a/frontend/src/app/components/clipboard/clipboard.component.ts +++ b/frontend/src/app/components/clipboard/clipboard.component.ts @@ -11,6 +11,8 @@ import * as tlite from 'tlite'; export class ClipboardComponent implements AfterViewInit { @ViewChild('btn') btn: ElementRef; @ViewChild('buttonWrapper') buttonWrapper: ElementRef; + @Input() button = false; + @Input() class = 'btn btn-secondary ml-1'; @Input() size: 'small' | 'normal' = 'normal'; @Input() text: string; @Input() leftPadding = true; diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts index f12277f72..7db1367ea 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts @@ -1,10 +1,10 @@ import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; +import { ApiService } from '../../services/api.service'; import { formatNumber } from '@angular/common'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; -import { StateService } from 'src/app/services/state.service'; +import { selectPowerOfTen } from '../../bitcoin.utils'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-difficulty-adjustments-table', diff --git a/frontend/src/app/components/fees-box/fees-box.component.ts b/frontend/src/app/components/fees-box/fees-box.component.ts index 20fe42647..48098db7b 100644 --- a/frontend/src/app/components/fees-box/fees-box.component.ts +++ b/frontend/src/app/components/fees-box/fees-box.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; -import { Recommendedfees } from 'src/app/interfaces/websocket.interface'; -import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; +import { Recommendedfees } from '../../interfaces/websocket.interface'; +import { feeLevels, mempoolFeeColors } from '../../app.constants'; import { tap } from 'rxjs/operators'; @Component({ diff --git a/frontend/src/app/components/footer/footer.component.ts b/frontend/src/app/components/footer/footer.component.ts index dbaa478d7..5e5b1f52a 100644 --- a/frontend/src/app/components/footer/footer.component.ts +++ b/frontend/src/app/components/footer/footer.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { Observable, combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; -import { MempoolInfo } from 'src/app/interfaces/websocket.interface'; +import { MempoolInfo } from '../../interfaces/websocket.interface'; interface MempoolBlocksData { blocks: number; diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index d6f9694d0..dd47a4ac7 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -31,17 +31,17 @@
Lightning nodes per network + i18n="lightning.nodes-networks">Lightning Nodes Per Network Network capacity + i18n="lightning.network-capacity">Lightning Network Capacity Lightning nodes per ISP + i18n="lightning.nodes-per-isp">Lightning Nodes Per ISP Lightning nodes per country + i18n="lightning.nodes-per-country">Lightning Nodes Per Country Lightning nodes world map + i18n="lightning.lightning.nodes-heatmap">Lightning Nodes World Map Lightning nodes channels world map + i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map
diff --git a/frontend/src/app/components/graphs/graphs.component.ts b/frontend/src/app/components/graphs/graphs.component.ts index 1d3a4e2ae..050b69848 100644 --- a/frontend/src/app/components/graphs/graphs.component.ts +++ b/frontend/src/app/components/graphs/graphs.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from "@angular/core"; -import { StateService } from "src/app/services/state.service"; -import { WebsocketService } from "src/app/services/websocket.service"; +import { Component, OnInit } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-graphs', diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 8952a27ce..50479f5d1 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -2,16 +2,16 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, H import { EChartsOption, graphic } from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { selectPowerOfTen } from '../../bitcoin.utils'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-hashrate-chart', diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index 716fc3216..dc0d5b5ed 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -2,13 +2,13 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, L import { EChartsOption } from 'echarts'; import { Observable } from 'rxjs'; import { delay, map, retryWhen, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { SeoService } from 'src/app/services/seo.service'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { poolsColor } from 'src/app/app.constants'; -import { StorageService } from 'src/app/services/storage.service'; -import { MiningService } from 'src/app/services/mining.service'; -import { download } from 'src/app/shared/graphs.utils'; +import { poolsColor } from '../../app.constants'; +import { StorageService } from '../../services/storage.service'; +import { MiningService } from '../../services/mining.service'; +import { download } from '../../shared/graphs.utils'; import { ActivatedRoute } from '@angular/router'; @Component({ diff --git a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts index 33ae2d320..d721469b7 100644 --- a/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts +++ b/frontend/src/app/components/incoming-transactions-graph/incoming-transactions-graph.component.ts @@ -1,8 +1,8 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { EChartsOption } from 'echarts'; import { OnChanges } from '@angular/core'; -import { StorageService } from 'src/app/services/storage.service'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { StorageService } from '../../services/storage.service'; +import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; import { formatNumber } from '@angular/common'; @Component({ diff --git a/frontend/src/app/components/language-selector/language-selector.component.ts b/frontend/src/app/components/language-selector/language-selector.component.ts index a2b10b7db..9fc2be6f7 100644 --- a/frontend/src/app/components/language-selector/language-selector.component.ts +++ b/frontend/src/app/components/language-selector/language-selector.component.ts @@ -1,8 +1,8 @@ import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { languages } from 'src/app/app.constants'; -import { LanguageService } from 'src/app/services/language.service'; +import { languages } from '../../app.constants'; +import { LanguageService } from '../../services/language.service'; @Component({ selector: 'app-language-selector', diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 7f22fd465..17f371202 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -49,13 +49,13 @@
diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index 22a351068..c57673529 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { merge, Observable, of} from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; -import { EnterpriseService } from 'src/app/services/enterprise.service'; +import { LanguageService } from '../../services/language.service'; +import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-liquid-master-page', @@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; network$: Observable; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit { this.connectionState$ = this.stateService.connectionState$; this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts index 3f59c2701..83a5ccc72 100644 --- a/frontend/src/app/components/loading-indicator/loading-indicator.component.ts +++ b/frontend/src/app/components/loading-indicator/loading-indicator.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { StateService } from 'src/app/services/state.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { StateService } from '../../services/state.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-loading-indicator', diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts index 61a392b5e..03a6a1ebb 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; +import { LanguageService } from '../../services/language.service'; @Component({ selector: 'app-master-page-preview', diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 1c28d5dce..5c365f0f9 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -22,13 +22,13 @@
diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 6ef6b86b2..8f7b4fecc 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -1,8 +1,9 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; -import { LanguageService } from 'src/app/services/language.service'; -import { EnterpriseService } from 'src/app/services/enterprise.service'; +import { LanguageService } from '../../services/language.service'; +import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-master-page', @@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; urlLanguage: string; subdomain = ''; + networkPaths: { [network: string]: string }; constructor( public stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit { this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); this.subdomain = this.enterpriseService.getSubdomain(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html index 304b2a7f9..3cb4ff3e8 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html @@ -3,7 +3,7 @@ [isLoading]="isLoading$ | async" [resolution]="75" [blockLimit]="stateService.blockVSize" - [orientation]="'left'" + [orientation]="timeLtr ? 'right' : 'left'" [flip]="true" (txClickEvent)="onTxClick($event)" > diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index bd78b13a9..7a39e3536 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -1,12 +1,12 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, - OnDestroy, OnChanges, ChangeDetectionStrategy, AfterViewInit } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; -import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; -import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; + OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface'; +import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; import { Subscription, BehaviorSubject, merge, of } from 'rxjs'; import { switchMap, filter } from 'rxjs/operators'; -import { WebsocketService } from 'src/app/services/websocket.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { WebsocketService } from '../../services/websocket.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Router } from '@angular/router'; @Component({ @@ -14,7 +14,7 @@ import { Router } from '@angular/router'; templateUrl: './mempool-block-overview.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, AfterViewInit { +export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { @Input() index: number; @Output() txPreviewEvent = new EventEmitter(); @@ -23,6 +23,10 @@ export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, Afte lastBlockHeight: number; blockIndex: number; isLoading$ = new BehaviorSubject(true); + timeLtrSubscription: Subscription; + timeLtr: boolean; + chainDirection: string = 'right'; + poolDirection: string = 'left'; blockSub: Subscription; deltaSub: Subscription; @@ -31,8 +35,18 @@ export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, Afte public stateService: StateService, private websocketService: WebsocketService, private router: Router, + private cd: ChangeDetectorRef, ) { } + ngOnInit(): void { + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + this.chainDirection = ltr ? 'left' : 'right'; + this.poolDirection = ltr ? 'right' : 'left'; + this.cd.markForCheck(); + }); + } + ngAfterViewInit(): void { this.blockSub = merge( of(true), @@ -50,7 +64,7 @@ export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, Afte ngOnChanges(changes): void { if (changes.index) { if (this.blockGraph) { - this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left'); + this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); } this.isLoading$.next(true); this.websocketService.startTrackMempoolBlock(changes.index.currentValue); @@ -60,16 +74,17 @@ export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, Afte ngOnDestroy(): void { this.blockSub.unsubscribe(); this.deltaSub.unsubscribe(); + this.timeLtrSubscription.unsubscribe(); this.websocketService.stopTrackMempoolBlock(); } replaceBlock(transactionsStripped: TransactionStripped[]): void { const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight); if (this.blockIndex !== this.index) { - const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right'; + const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; this.blockGraph.enter(transactionsStripped, direction); } else { - this.blockGraph.replace(transactionsStripped, blockMined ? 'right' : 'left'); + this.blockGraph.replace(transactionsStripped, blockMined ? this.chainDirection : this.poolDirection); } this.lastBlockHeight = this.stateService.latestBlockHeight; @@ -81,10 +96,10 @@ export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, Afte const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight); if (this.blockIndex !== this.index) { - const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right'; + const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; this.blockGraph.replace(delta.added, direction); } else { - this.blockGraph.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined); + this.blockGraph.update(delta.added, delta.removed, blockMined ? this.chainDirection : this.poolDirection, blockMined); } this.lastBlockHeight = this.stateService.latestBlockHeight; diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 75e171f2b..b9bdc55bb 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { switchMap, map, tap, filter } from 'rxjs/operators'; -import { MempoolBlock, TransactionStripped } from 'src/app/interfaces/websocket.interface'; +import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface'; import { Observable, BehaviorSubject } from 'rxjs'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-mempool-block', diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 77ca95b2f..9e70c6e74 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -1,5 +1,5 @@ -
+
@@ -45,7 +45,7 @@ -
+
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss index c41cde3fc..565d4b302 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.scss @@ -1,7 +1,7 @@ .bitcoin-block { width: 125px; height: 125px; - transition: 2s; + transition: background 2s, right 2s, transform 1s; } .block-size { @@ -33,6 +33,7 @@ .block-body { text-align: center; + transition: transform 1s; } @keyframes opacityPulse { @@ -73,6 +74,7 @@ background-color: #232838; transform:skew(40deg); transform-origin:top; + transition: transform 1s, left 1s; } .bitcoin-block::before { @@ -83,9 +85,11 @@ top: -12px; left: -20px; background-color: #191c27; + z-index: -1; transform: skewY(50deg); transform-origin: top; + transition: transform 1s, left 1s; } .mempool-block.bitcoin-block::after { @@ -128,3 +132,24 @@ .blockLink:hover { text-decoration: none; } + +.time-ltr { + .bitcoin-block::after { + transform: skew(-40deg); + left: 20px; + } + + .bitcoin-block::before { + transform: skewY(-50deg); + left: 125px; + } + .block-body { + transform: scaleX(-1); + } +} + +:host-context(.rtl-layout) { + #arrow-up { + transform: translateX(70px); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 0019c8a44..e1a443680 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core'; import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs'; -import { MempoolBlock } from 'src/app/interfaces/websocket.interface'; -import { StateService } from 'src/app/services/state.service'; +import { MempoolBlock } from '../../interfaces/websocket.interface'; +import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; import { take, map, switchMap } from 'rxjs/operators'; -import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; -import { specialBlocks } from 'src/app/app.constants'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { feeLevels, mempoolFeeColors } from '../../app.constants'; +import { specialBlocks } from '../../app.constants'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; -import { DifficultyAdjustment } from 'src/app/interfaces/node-api.interface'; +import { DifficultyAdjustment } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-mempool-blocks', @@ -36,6 +36,8 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { now = new Date().getTime(); timeOffset = 0; showMiningInfo = false; + timeLtrSubscription: Subscription; + timeLtr: boolean; blockWidth = 125; blockPadding = 30; @@ -44,7 +46,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { feeRounding = '1.0-0'; rightPosition = 0; - transition = '2s'; + transition = 'background 2s, right 2s, transform 1s'; markIndex: number; txFeePerVSize: number; @@ -72,6 +74,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url)); } + this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { + this.timeLtr = !!ltr; + this.cd.markForCheck(); + }); + if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') { this.feeRounding = '1.0-1'; } @@ -160,8 +167,10 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { if (this.markIndex === undefined) { return; } + const prevKey = this.timeLtr ? 'ArrowLeft' : 'ArrowRight'; + const nextKey = this.timeLtr ? 'ArrowRight' : 'ArrowLeft'; - if (event.key === 'ArrowRight') { + if (event.key === prevKey) { if (this.mempoolBlocks[this.markIndex - 1]) { this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]); } else { @@ -173,7 +182,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { } }); } - } else if (event.key === 'ArrowLeft') { + } else if (event.key === nextKey) { if (this.mempoolBlocks[this.markIndex + 1]) { this.router.navigate([this.relativeUrlPipe.transform('/mempool-block/'), this.markIndex + 1]); } @@ -185,6 +194,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.markBlocksSubscription.unsubscribe(); this.blockSubscription.unsubscribe(); this.networkSubscription.unsubscribe(); + this.timeLtrSubscription.unsubscribe(); clearTimeout(this.resetTransitionTimeout); } @@ -269,7 +279,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.arrowVisible = true; this.resetTransitionTimeout = window.setTimeout(() => { - this.transition = '2s'; + this.transition = 'background 2s, right 2s, transform 1s'; this.cd.markForCheck(); }, 100); return; @@ -277,11 +287,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { this.arrowVisible = true; - for (const block of this.mempoolBlocks) { - for (let i = 0; i < block.feeRange.length - 1; i++) { + let found = false; + for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) { + const block = this.mempoolBlocks[txInBlockIndex]; + for (let i = 0; i < block.feeRange.length - 1 && !found; i++) { if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) { - const txInBlockIndex = this.mempoolBlocks.indexOf(block); - const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]); + const feeRangeIndex = i; const feeRangeChunkSize = 1 / (block.feeRange.length - 1); const txFee = this.txFeePerVSize - block.feeRange[i]; @@ -296,9 +307,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth); this.rightPosition = arrowRightPosition; - break; + found = true; } } + if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) { + this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding); + found = true; + } } } diff --git a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts index 04c7ddf69..989fa141e 100644 --- a/frontend/src/app/components/mempool-graph/mempool-graph.component.ts +++ b/frontend/src/app/components/mempool-graph/mempool-graph.component.ts @@ -1,12 +1,12 @@ import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core'; -import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe'; +import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe'; import { formatNumber } from '@angular/common'; -import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface'; -import { StateService } from 'src/app/services/state.service'; -import { StorageService } from 'src/app/services/storage.service'; +import { OptimizedMempoolStats } from '../../interfaces/node-api.interface'; +import { StateService } from '../../services/state.service'; +import { StorageService } from '../../services/storage.service'; import { EChartsOption } from 'echarts'; -import { feeLevels, chartColors } from 'src/app/app.constants'; -import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { feeLevels, chartColors } from '../../app.constants'; +import { download, formatterXAxis, formatterXAxisLabel } from '../../shared/graphs.utils'; @Component({ selector: 'app-mempool-graph', diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index 15ffe2c87..df4713374 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { SeoService } from 'src/app/services/seo.service'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from '../../services/websocket.service'; @Component({ selector: 'app-mining-dashboard', diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index 820699d2b..57542fd30 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -4,15 +4,15 @@ import { ActivatedRoute, Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; import { concat, Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; -import { SinglePoolStats } from 'src/app/interfaces/node-api.interface'; -import { SeoService } from 'src/app/services/seo.service'; +import { SinglePoolStats } from '../../interfaces/node-api.interface'; +import { SeoService } from '../../services/seo.service'; import { StorageService } from '../..//services/storage.service'; import { MiningService, MiningStats } from '../../services/mining.service'; import { StateService } from '../../services/state.service'; -import { chartColors, poolsColor } from 'src/app/app.constants'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { download } from 'src/app/shared/graphs.utils'; -import { isMobile } from 'src/app/shared/common.utils'; +import { chartColors, poolsColor } from '../../app.constants'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { download } from '../../shared/graphs.utils'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-pool-ranking', diff --git a/frontend/src/app/components/pool/pool-preview.component.html b/frontend/src/app/components/pool/pool-preview.component.html new file mode 100644 index 000000000..ff5201ae0 --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.html @@ -0,0 +1,34 @@ +
+ + mining pool + +
+
+ + +
+
+

{{ poolStats.pool.name }}

+
+
+
+
+
+
Tags
+
{{ poolStats.pool.regexes }}
+
+
+
Hashrate
+
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}
+
+
+
+
+
+
+
+ + +
~
+
\ No newline at end of file diff --git a/frontend/src/app/components/pool/pool-preview.component.scss b/frontend/src/app/components/pool/pool-preview.component.scss new file mode 100644 index 000000000..bd0c19859 --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.scss @@ -0,0 +1,78 @@ +.stats { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + width: 100%; + max-width: 100%; + margin: 15px 0; + font-size: 32px; + overflow: hidden; + + .stat-box { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: baseline; + justify-content: space-between; + width: 100%; + margin-left: 15px; + background: #181b2d; + padding: 0.75rem; + width: 0; + flex-grow: 1; + + &:first-child { + margin-left: 0; + } + + .label { + flex-shrink: 0; + flex-grow: 0; + margin-right: 1em; + } + .data { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.chart { + width: 100%; + height: 315px; + background: #181b2d; +} + +.row { + margin-right: 0; +} + +.full-width-row { + padding-left: 15px; + flex-wrap: nowrap; +} + +.logo-wrapper { + position: relative; + width: 62px; + height: 62px; + margin-right: 1em; + + img { + position: absolute; + right: 0; + top: 0; + background: #24273e; + + &.noimg { + opacity: 0; + } + } +} + +::ng-deep .symbol { + font-size: 24px; +} diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts new file mode 100644 index 000000000..277bacb33 --- /dev/null +++ b/frontend/src/app/components/pool/pool-preview.component.ts @@ -0,0 +1,187 @@ +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable, of } from 'rxjs'; +import { map, switchMap, catchError } from 'rxjs/operators'; +import { PoolStat } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { formatNumber } from '@angular/common'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; + +@Component({ + selector: 'app-pool-preview', + templateUrl: './pool-preview.component.html', + styleUrls: ['./pool-preview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PoolPreviewComponent implements OnInit { + formatNumber = formatNumber; + poolStats$: Observable; + isLoading = true; + imageLoaded = false; + lastImgSrc: string = ''; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + slug: string = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private apiService: ApiService, + private route: ActivatedRoute, + public stateService: StateService, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { + } + + ngOnInit(): void { + this.poolStats$ = this.route.params.pipe(map((params) => params.slug)) + .pipe( + switchMap((slug: any) => { + this.isLoading = true; + this.imageLoaded = false; + this.slug = slug; + this.openGraphService.waitFor('pool-hash-' + this.slug); + this.openGraphService.waitFor('pool-stats-' + this.slug); + this.openGraphService.waitFor('pool-chart-' + this.slug); + this.openGraphService.waitFor('pool-img-' + this.slug); + return this.apiService.getPoolHashrate$(this.slug) + .pipe( + switchMap((data) => { + this.isLoading = false; + this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate])); + this.openGraphService.waitOver('pool-hash-' + this.slug); + return [slug]; + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-hash-' + this.slug); + return of([slug]); + }) + ); + }), + switchMap((slug) => { + return this.apiService.getPoolStats$(slug).pipe( + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + }), + map((poolStats) => { + if (poolStats == null) { + return null; + } + + this.seoService.setTitle(poolStats.pool.name); + let regexes = '"'; + for (const regex of poolStats.pool.regexes) { + regexes += regex + '", "'; + } + poolStats.pool.regexes = regexes.slice(0, -3); + poolStats.pool.addresses = poolStats.pool.addresses; + + if (poolStats.reportedHashrate) { + poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100; + } + + this.openGraphService.waitOver('pool-stats-' + this.slug); + + const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + if (logoSrc === this.lastImgSrc) { + this.openGraphService.waitOver('pool-img-' + this.slug); + } + this.lastImgSrc = logoSrc; + return Object.assign({ + logo: logoSrc + }, poolStats); + }), + catchError(() => { + this.isLoading = false; + this.openGraphService.fail('pool-stats-' + this.slug); + return of(null); + }) + ); + } + + prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ]), + '#D81B60', + ], + grid: { + left: 15, + right: 15, + bottom: 15, + top: 15, + show: false, + }, + xAxis: data.length === 0 ? undefined : { + type: 'time', + show: false, + }, + yAxis: data.length === 0 ? undefined : [ + { + type: 'value', + show: false, + }, + ], + series: data.length === 0 ? undefined : [ + { + zlevel: 0, + name: 'Hashrate', + showSymbol: false, + symbol: 'none', + data: data, + type: 'line', + lineStyle: { + width: 4, + }, + }, + ], + }; + } + + onChartReady(): void { + this.openGraphService.waitOver('pool-chart-' + this.slug); + } + + onImageLoad(): void { + this.imageLoaded = true; + this.openGraphService.waitOver('pool-img-' + this.slug); + } + + onImageFail(): void { + this.imageLoaded = false; + this.openGraphService.waitOver('pool-img-' + this.slug); + } +} diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts index 7a37bf7bd..56b8bd392 100644 --- a/frontend/src/app/components/pool/pool.component.ts +++ b/frontend/src/app/components/pool/pool.component.ts @@ -3,12 +3,12 @@ import { ActivatedRoute } from '@angular/router'; import { EChartsOption, graphic } from 'echarts'; import { BehaviorSubject, Observable, timer } from 'rxjs'; import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, PoolStat } from 'src/app/interfaces/node-api.interface'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; -import { selectPowerOfTen } from 'src/app/bitcoin.utils'; +import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { selectPowerOfTen } from '../../bitcoin.utils'; import { formatNumber } from '@angular/common'; -import { SeoService } from 'src/app/services/seo.service'; +import { SeoService } from '../../services/seo.service'; @Component({ selector: 'app-pool', diff --git a/frontend/src/app/components/push-transaction/push-transaction.component.ts b/frontend/src/app/components/push-transaction/push-transaction.component.ts index 294f0591a..a4eb375a6 100644 --- a/frontend/src/app/components/push-transaction/push-transaction.component.ts +++ b/frontend/src/app/components/push-transaction/push-transaction.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { ApiService } from 'src/app/services/api.service'; +import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-push-transaction', diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index 923e66fd8..e8ebac904 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -1,6 +1,6 @@ import { Component, Input, AfterViewInit, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'; import * as QRCode from 'qrcode'; -import { StateService } from 'src/app/services/state.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-qrcode', diff --git a/frontend/src/app/components/reward-stats/reward-stats.component.ts b/frontend/src/app/components/reward-stats/reward-stats.component.ts index c92ac757d..1eda26cce 100644 --- a/frontend/src/app/components/reward-stats/reward-stats.component.ts +++ b/frontend/src/app/components/reward-stats/reward-stats.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { concat, Observable } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { ApiService } from 'src/app/services/api.service'; -import { StateService } from 'src/app/services/state.service'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-reward-stats', diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 8badcc3cf..1303f4a62 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,9 +1,9 @@
- + - +
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 712f7438c..15a75652d 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,13 +1,13 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { AssetsService } from 'src/app/services/assets.service'; -import { StateService } from 'src/app/services/state.service'; -import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators'; -import { ElectrsApiService } from 'src/app/services/electrs-api.service'; -import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; -import { ApiService } from 'src/app/services/api.service'; +import { AssetsService } from '../../services/assets.service'; +import { StateService } from '../../services/state.service'; +import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { ApiService } from '../../services/api.service'; import { SearchResultsComponent } from './search-results/search-results.component'; @Component({ @@ -24,7 +24,7 @@ export class SearchFormComponent implements OnInit { typeAhead$: Observable; searchForm: FormGroup; - regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/; + regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/; regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; regexBlockheight = /^[0-9]{1,9}$/; @@ -33,7 +33,7 @@ export class SearchFormComponent implements OnInit { @Output() searchTriggered = new EventEmitter(); @ViewChild('searchResults') searchResults: SearchResultsComponent; - @HostListener('keydown', ['$event']) keydown($event) { + @HostListener('keydown', ['$event']) keydown($event): void { this.handleKeyDown($event); } @@ -47,7 +47,7 @@ export class SearchFormComponent implements OnInit { private relativeUrlPipe: RelativeUrlPipe, ) { } - ngOnInit() { + ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.searchForm = this.formBuilder.group({ @@ -61,66 +61,115 @@ export class SearchFormComponent implements OnInit { }); } - this.typeAhead$ = this.searchForm.get('searchText').valueChanges - .pipe( - map((text) => { - if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { - return text.substr(1); - } - return text.trim(); - }), - debounceTime(200), - distinctUntilChanged(), - switchMap((text) => { - if (!text.length) { - return of([ - [], - { - nodes: [], - channels: [], - } - ]); - } - this.isTypeaheading$.next(true); - if (!this.stateService.env.LIGHTNING) { - return zip( - this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - [{ nodes: [], channels: [] }] - ); - } + const searchText$ = this.searchForm.get('searchText').valueChanges + .pipe( + map((text) => { + if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { + return text.substr(1); + } + return text.trim(); + }), + distinctUntilChanged(), + ); + + const searchResults$ = searchText$.pipe( + debounceTime(200), + switchMap((text) => { + if (!text.length) { + return of([ + [], + { nodes: [], channels: [] } + ]); + } + this.isTypeaheading$.next(true); + if (!this.stateService.env.LIGHTNING) { return zip( this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - this.apiService.lightningSearch$(text).pipe(catchError(() => of({ + [{ nodes: [], channels: [] }], + ); + } + return zip( + this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), + this.apiService.lightningSearch$(text).pipe(catchError(() => of({ + nodes: [], + channels: [], + }))), + ); + }), + tap((result: any[]) => { + this.isTypeaheading$.next(false); + }) + ); + + this.typeAhead$ = combineLatest( + [ + searchText$, + searchResults$.pipe( + startWith([ + [], + { + nodes: [], + channels: [], + } + ])) + ] + ).pipe( + map((latestData) => { + const searchText = latestData[0]; + if (!searchText.length) { + return { + searchText: '', + hashQuickMatch: false, + blockHeight: false, + txId: false, + address: false, + addresses: [], nodes: [], channels: [], - }))), - ); - }), - map((result: any[]) => { - this.isTypeaheading$.next(false); - if (this.network === 'bisq') { - return result[0].map((address: string) => 'B' + address); + }; } + + const result = latestData[1]; + const addressPrefixSearchResults = result[0]; + const lightningResults = result[1]; + + if (this.network === 'bisq') { + return searchText.map((address: string) => 'B' + address); + } + + const matchesBlockHeight = this.regexBlockheight.test(searchText); + const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); + const matchesBlockHash = this.regexBlockhash.test(searchText); + const matchesAddress = this.regexAddress.test(searchText); + return { - addresses: result[0], - nodes: result[1].nodes, - channels: result[1].channels, - totalResults: result[0].length + result[1].nodes.length + result[1].channels.length, + searchText: searchText, + hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress), + blockHeight: matchesBlockHeight, + txId: matchesTxId, + blockHash: matchesBlockHash, + address: matchesAddress, + addresses: addressPrefixSearchResults, + nodes: lightningResults.nodes, + channels: lightningResults.channels, }; }) ); } - handleKeyDown($event) { + + handleKeyDown($event): void { this.searchResults.handleKeyDown($event); } - itemSelected() { + itemSelected(): void { setTimeout(() => this.search()); } - selectedResult(result: any) { + selectedResult(result: any): void { if (typeof result === 'string') { this.search(result); + } else if (typeof result === 'number') { + this.navigate('/block/', result.toString()); } else if (result.alias) { this.navigate('/lightning/node/', result.public_key); } else if (result.short_id) { @@ -128,7 +177,7 @@ export class SearchFormComponent implements OnInit { } } - search(result?: string) { + search(result?: string): void { const searchText = result || this.searchForm.value.searchText.trim(); if (searchText) { this.isSearching = true; @@ -162,7 +211,7 @@ export class SearchFormComponent implements OnInit { } } - navigate(url: string, searchText: string, extras?: any) { + navigate(url: string, searchText: string, extras?: any): void { this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); this.searchTriggered.emit(); this.searchForm.setValue({ diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index cc289ddac..a13228170 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -1,25 +1,49 @@ -