diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 7cace626c..e8f6d1df1 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -2,7 +2,7 @@ name: Cypress Tests on: pull_request: - types: [ opened, review_requested, synchronize ] + types: [opened, review_requested, synchronize] jobs: cypress: if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" @@ -24,36 +24,36 @@ jobs: - module: "bisq" spec: | cypress/e2e/bisq/bisq.spec.ts - + name: E2E tests for ${{ matrix.module }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ${{ matrix.module }} - + - name: Setup node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: 16.15.0 - cache: 'npm' + cache: "npm" cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json - name: Chrome browser tests (${{ matrix.module }}) - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@v5 with: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} start: npm run start:local-staging - wait-on: 'http://localhost:4200' + wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true parallel: true spec: ${{ matrix.spec }} group: Tests on Chrome (${{ matrix.module }}) browser: "chrome" - ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}' + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" env: COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 32d4b085b..5d8d71104 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -31,7 +31,7 @@ jobs: run: | sudo swapoff /mnt/swapfile sudo rm -v /mnt/swapfile - sudo fallocate -l 10G /mnt/swapfile + sudo fallocate -l 13G /mnt/swapfile sudo chmod 600 /mnt/swapfile sudo mkswap /mnt/swapfile sudo swapon /mnt/swapfile @@ -68,24 +68,24 @@ jobs: run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - name: Checkout project - uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0 + uses: actions/checkout@v3 - name: Init repo for Dockerization run: docker/init.sh "$TAG" - name: Set up QEMU - uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0 + uses: docker/setup-qemu-action@v2 id: qemu - name: Setup Docker buildx action - uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1 + uses: docker/setup-buildx-action@v2 id: buildx - name: Available platforms run: echo ${{ steps.buildx.outputs.platforms }} - name: Cache Docker layers - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11 + uses: actions/cache@v3 id: cache with: path: /tmp/.buildx-cache diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index dbbc8412d..1f64214ce 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -27,7 +27,7 @@ "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_MEMPOOL": false, - "TRANSACTION_INDEXING": false + "CPFP_INDEXING": false }, "CORE_RPC": { "HOST": "127.0.0.1", diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 2e9221c7a..e699c9458 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -26,9 +26,9 @@ "INDEXING_BLOCKS_AMOUNT": 14, "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__POOLS_JSON_URL__", - "ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__", - "ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__", - "TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__" + "ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", + "ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", + "CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__" }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 58cf3a214..4158d3df1 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -40,7 +40,7 @@ describe('Mempool Backend Config', () => { POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json', ADVANCED_GBT_AUDIT: false, ADVANCED_GBT_MEMPOOL: false, - TRANSACTION_INDEXING: false, + CPFP_INDEXING: false, }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index 6aafc9ded..b6b36dbdc 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -1,10 +1,5 @@ import config from '../config'; -import bitcoinApi from './bitcoin/bitcoin-api-factory'; -import { Common } from './common'; -import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces'; -import blocksRepository from '../repositories/BlocksRepository'; -import blocksAuditsRepository from '../repositories/BlocksAuditsRepository'; -import blocks from '../api/blocks'; +import { 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 diff --git a/backend/src/api/backend-info.ts b/backend/src/api/backend-info.ts index 57bb5fe13..fc3181524 100644 --- a/backend/src/api/backend-info.ts +++ b/backend/src/api/backend-info.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import { IBackendInfo } from '../mempool.interfaces'; +import config from '../config'; class BackendInfo { private backendInfo: IBackendInfo; @@ -22,7 +23,8 @@ class BackendInfo { this.backendInfo = { hostname: os.hostname(), version: versionInfo.version, - gitCommit: versionInfo.gitCommit + gitCommit: versionInfo.gitCommit, + lightning: config.LIGHTNING.ENABLED }; } diff --git a/backend/src/api/bitcoin/bitcoin-api-factory.ts b/backend/src/api/bitcoin/bitcoin-api-factory.ts index f89d07b50..24916b97b 100644 --- a/backend/src/api/bitcoin/bitcoin-api-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-factory.ts @@ -17,4 +17,6 @@ function bitcoinApiFactory(): AbstractBitcoinApi { } } +export const bitcoinCoreApi = new BitcoinApi(bitcoinClient); + export default bitcoinApiFactory(); diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 55500d0c9..2d77969a1 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -402,7 +402,8 @@ class BitcoinRoutes { private async getLegacyBlocks(req: Request, res: Response) { try { const returnBlocks: IEsploraApi.Block[] = []; - const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight(); + const tip = blocks.getCurrentBlockHeight(); + const fromHeight = Math.min(parseInt(req.params.height, 10) || tip, tip); // Check if block height exist in local cache to skip the hash lookup const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 8c8272262..83de897ca 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -22,12 +22,10 @@ import poolsParser from './pools-parser'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import cpfpRepository from '../repositories/CpfpRepository'; -import transactionRepository from '../repositories/TransactionRepository'; import mining from './mining/mining'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import PricesRepository from '../repositories/PricesRepository'; import priceUpdater from '../tasks/price-updater'; -import { Block } from 'bitcoinjs-lib'; class Blocks { private blocks: BlockExtended[] = []; @@ -101,12 +99,23 @@ class Blocks { transactions.push(tx); transactionsFetched++; } catch (e) { - if (i === 0) { - const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e); - logger.err(msg); - throw new Error(msg); - } else { - logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); + try { + if (config.MEMPOOL.BACKEND === 'esplora') { + // Try again with core + const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true); + transactions.push(tx); + transactionsFetched++; + } else { + throw e; + } + } catch (e) { + if (i === 0) { + const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e); + logger.err(msg); + throw new Error(msg); + } else { + logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); + } } } } @@ -296,7 +305,7 @@ class Blocks { const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100; - logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`); + logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); timer = new Date().getTime() / 1000; indexedThisRun = 0; } @@ -309,12 +318,12 @@ class Blocks { newlyIndexed++; } if (newlyIndexed > 0) { - logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); + logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); } else { - logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); + logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); } } catch (e) { - logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); throw e; } } @@ -329,9 +338,10 @@ class Blocks { try { // Get all indexed block hash - const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks(); + const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks(); + logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`); - if (!unindexedBlocks?.length) { + if (!unindexedBlockHeights?.length) { return; } @@ -340,30 +350,26 @@ class Blocks { let countThisRun = 0; let timer = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000; - - for (const block of unindexedBlocks) { + for (const height of unindexedBlockHeights) { // Logging + const hash = await bitcoinApi.$getBlockHash(height); const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer); if (elapsedSeconds > 5) { const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); - const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds); - const progress = Math.round(count / unindexedBlocks.length * 10000) / 100; - logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`); + const blockPerSeconds = (countThisRun / elapsedSeconds); + const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100; + logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`); timer = new Date().getTime() / 1000; countThisRun = 0; } - await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block + await this.$indexCPFP(hash, height); // Calculate and save CPFP data for transactions in this block // Logging count++; countThisRun++; } - if (count > 0) { - logger.notice(`CPFP indexing completed: indexed ${count} blocks`); - } else { - logger.debug(`CPFP indexing completed: indexed ${count} blocks`); - } + logger.notice(`CPFP indexing completed: indexed ${count} blocks`); } catch (e) { logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); throw e; @@ -385,7 +391,7 @@ class Blocks { const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); - logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`); + logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`, logger.tags.mining); loadingIndicators.setProgress('block-indexing', 0); const chunkSize = 10000; @@ -405,7 +411,7 @@ class Blocks { continue; } - logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`); + logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining); for (const blockHeight of missingBlockHeights) { if (blockHeight < lastBlockToIndex) { @@ -418,7 +424,7 @@ class Blocks { const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100; - logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`); + logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); timer = new Date().getTime() / 1000; indexedThisRun = 0; loadingIndicators.setProgress('block-indexing', progress, false); @@ -435,13 +441,13 @@ class Blocks { currentBlockHeight -= chunkSize; } if (newlyIndexed > 0) { - logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`); + logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); } else { - logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`); + logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining); } loadingIndicators.setProgress('block-indexing', 100); } catch (e) { - logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); loadingIndicators.setProgress('block-indexing', 100); throw e; } @@ -519,7 +525,7 @@ class Blocks { for (let i = 10; i >= 0; --i) { const newBlock = await this.$indexBlock(lastBlock['height'] - i); await this.$getStrippedBlockTransactions(newBlock.id, true, true); - if (config.MEMPOOL.TRANSACTION_INDEXING) { + if (config.MEMPOOL.CPFP_INDEXING) { await this.$indexCPFP(newBlock.id, lastBlock['height'] - i); } } @@ -537,7 +543,7 @@ class Blocks { priceId: lastestPriceId, }]); } else { - logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`) + logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); setTimeout(() => { indexer.runSingleTask('blocksPrices'); }, 10000); @@ -547,7 +553,7 @@ class Blocks { if (Common.blocksSummariesIndexingEnabled() === true) { await this.$getStrippedBlockTransactions(blockExtended.id, true); } - if (config.MEMPOOL.TRANSACTION_INDEXING) { + if (config.MEMPOOL.CPFP_INDEXING) { this.$indexCPFP(blockExtended.id, this.currentBlockHeight); } } @@ -677,7 +683,12 @@ class Blocks { } public async $getBlocks(fromHeight?: number, limit: number = 15): Promise { - let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight(); + + let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; + if (currentHeight > this.currentBlockHeight) { + limit -= currentHeight - this.currentBlockHeight; + currentHeight = this.currentBlockHeight; + } const returnBlocks: BlockExtended[] = []; if (currentHeight < 0) { @@ -741,32 +752,15 @@ class Blocks { } public async $indexCPFP(hash: string, height: number): Promise { - let transactions; - if (false/*Common.blocksSummariesIndexingEnabled()*/) { - transactions = await this.$getStrippedBlockTransactions(hash); - const rawBlock = await bitcoinApi.$getRawBlock(hash); - const block = Block.fromBuffer(rawBlock); - const txMap = {}; - for (const tx of block.transactions || []) { - txMap[tx.getId()] = tx; - } - for (const tx of transactions) { - if (txMap[tx.txid]?.ins) { - tx.vin = txMap[tx.txid].ins.map(vin => { - return { - txid: vin.hash - }; - }); - } - } - } else { - const block = await bitcoinClient.getBlock(hash, 2); - transactions = block.tx.map(tx => { - tx.vsize = tx.weight / 4; - return tx; - }); - } - + const block = await bitcoinClient.getBlock(hash, 2); + const transactions = block.tx.map(tx => { + tx.vsize = tx.weight / 4; + tx.fee *= 100_000_000; + return tx; + }); + + const clusters: any[] = []; + let cluster: TransactionStripped[] = []; let ancestors: { [txid: string]: boolean } = {}; for (let i = transactions.length - 1; i >= 0; i--) { @@ -778,12 +772,14 @@ class Blocks { totalFee += tx?.fee || 0; totalVSize += tx.vsize; }); - const effectiveFeePerVsize = (totalFee * 100_000_000) / totalVSize; + const effectiveFeePerVsize = totalFee / totalVSize; if (cluster.length > 1) { - await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: (tx.fee || 0) * 100_000_000 }; }), effectiveFeePerVsize); - for (const tx of cluster) { - await transactionRepository.$setCluster(tx.txid, cluster[0].txid); - } + clusters.push({ + root: cluster[0].txid, + height, + txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), + effectiveFeePerVsize, + }); } cluster = []; ancestors = {}; @@ -793,7 +789,10 @@ class Blocks { ancestors[vin.txid] = true; }); } - await blocksRepository.$setCPFPIndexed(hash); + const result = await cpfpRepository.$batchSaveClusters(clusters); + if (!result) { + await cpfpRepository.$insertProgressMarker(height); + } } } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 621f021ba..f0c5c6b88 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -190,7 +190,7 @@ export class Common { static cpfpIndexingEnabled(): boolean { return ( Common.indexingEnabled() && - config.MEMPOOL.TRANSACTION_INDEXING === true + config.MEMPOOL.CPFP_INDEXING === true ); } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 6e0e95699..42f223417 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -2,9 +2,12 @@ import config from '../config'; import DB from '../database'; import logger from '../logger'; import { Common } from './common'; +import blocksRepository from '../repositories/BlocksRepository'; +import cpfpRepository from '../repositories/CpfpRepository'; +import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 49; + private static currentVersion = 52; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -442,6 +445,29 @@ class DatabaseMigration { await this.$executeQuery('TRUNCATE TABLE `blocks_audits`'); await this.updateToSchemaVersion(49); } + + if (databaseSchemaVersion < 50) { + await this.$executeQuery('ALTER TABLE `blocks` DROP COLUMN `cpfp_indexed`'); + await this.updateToSchemaVersion(50); + } + + if (databaseSchemaVersion < 51) { + await this.$executeQuery('ALTER TABLE `cpfp_clusters` ADD INDEX `height` (`height`)'); + await this.updateToSchemaVersion(51); + } + + if (databaseSchemaVersion < 52) { + await this.$executeQuery(this.getCreateCompactCPFPTableQuery(), await this.$checkIfTableExists('compact_cpfp_clusters')); + await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions')); + try { + await this.$convertCompactCpfpTables(); + await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`'); + await this.$executeQuery('DROP TABLE IF EXISTS `transactions`'); + await this.updateToSchemaVersion(52); + } catch(e) { + logger.warn('' + (e instanceof Error ? e.message : e)); + } + } } /** @@ -913,6 +939,25 @@ class DatabaseMigration { ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + private getCreateCompactCPFPTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS compact_cpfp_clusters ( + root binary(32) NOT NULL, + height int(10) NOT NULL, + txs BLOB DEFAULT NULL, + fee_rate float unsigned, + PRIMARY KEY (root), + INDEX (height) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + + private getCreateCompactTransactionsTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS compact_transactions ( + txid binary(32) NOT NULL, + cluster binary(32) DEFAULT NULL, + PRIMARY KEY (txid) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } + public async $truncateIndexedData(tables: string[]) { const allowedTables = ['blocks', 'hashrates', 'prices']; @@ -933,6 +978,49 @@ class DatabaseMigration { logger.warn(`Unable to erase indexed data`); } } + + private async $convertCompactCpfpTables(): Promise { + try { + const batchSize = 250; + const maxHeight = await blocksRepository.$mostRecentBlockHeight() || 0; + const [minHeightRows]: any = await DB.query(`SELECT MIN(height) AS minHeight from cpfp_clusters`); + const minHeight = (minHeightRows.length && minHeightRows[0].minHeight != null) ? minHeightRows[0].minHeight : maxHeight; + let height = maxHeight; + + // Logging + let timer = new Date().getTime() / 1000; + const startedAt = new Date().getTime() / 1000; + + while (height > minHeight) { + const [rows] = await DB.query( + ` + SELECT * from cpfp_clusters + WHERE height <= ? AND height > ? + ORDER BY height + `, + [height, height - batchSize] + ) as RowDataPacket[][]; + if (rows?.length) { + await cpfpRepository.$batchSaveClusters(rows.map(row => { + return { + root: row.root, + height: row.height, + txs: JSON.parse(row.txs), + effectiveFeePerVsize: row.fee_rate, + }; + })); + } + + const elapsed = new Date().getTime() / 1000 - timer; + const runningFor = new Date().getTime() / 1000 - startedAt; + logger.debug(`Migrated cpfp data from block ${height} to ${height - batchSize} in ${elapsed.toFixed(2)} seconds | total elapsed: ${runningFor.toFixed(2)} seconds`); + timer = new Date().getTime() / 1000; + height -= batchSize; + } + } catch (e) { + logger.warn(`Failed to migrate cpfp transaction data`); + } + } } export default new DatabaseMigration(); diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index e761288a3..8314b3345 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -670,9 +670,7 @@ class ChannelsApi { AND status != 2 `); if (result[0].changedRows ?? 0 > 0) { - logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`); - } else { - logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`); + logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`, logger.tags.ln); } } catch (e) { logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index cf1c77686..d292aa2e3 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -685,9 +685,7 @@ class NodesApi { ) `); if (result[0].changedRows ?? 0 > 0) { - logger.info(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`); - } else { - logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`); + logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`, logger.tags.ln); } } catch (e) { logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e)); diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index c19bde236..e2dbcb0b6 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -41,13 +41,70 @@ class NodesRoutes { let nodes: any[] = []; switch (config.MEMPOOL.NETWORK) { case 'testnet': - nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584']; + nodesList = [ + '032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', + '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', + '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', + '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', + '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', + '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', + '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', + '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', + '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', + '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', + '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', + '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584', + '0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c', + '029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5', + '02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075', + '030b0ca1ea7b1075716d2a555630e6fd47ef11bc7391fe68963ec06cf370a5e382', + '031adb9eb2d66693f85fa31a4adca0319ba68219f3ad5f9a2ef9b34a6b40755fa1', + '02ccd07faa47eda810ecf5591ccf5ca50f6c1034d0d175052898d32a00b9bae24f', + ]; break; case 'signet': - nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7']; + nodesList = [ + '03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', + '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', + '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', + '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', + '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', + '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', + '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', + '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', + '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', + '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', + '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', + '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7', + '02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070', + '02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45', + '038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097', + '0242c7f7d315095f37ad1421ae0a2fc967d4cbe65b61b079c5395a769436959853', + '02a909e70eb03742f12666ebb1f56ac42a5fbaab0c0e8b5b1df4aa9f10f8a09240', + '03a26efa12489803c07f3ac2f1dba63812e38f0f6e866ce3ebb34df7de1f458cd2', + ]; break; default: - nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43']; + nodesList = [ + '03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', + '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', + '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', + '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', + '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', + '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', + '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', + '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', + '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', + '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', + '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', + '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43', + '02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06', + '0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e', + '03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7', + '038d118996b3eaa15dcd317b32a539c9ecfdd7698f204acf8a087336af655a9192', + '02a928903d93d78877dacc3642b696128a3636e9566dd42d2d132325b2c8891c09', + '0328cd17f3a9d3d90b532ade0d1a67e05eb8a51835b3dce0a2e38eac04b5a62a57', + ]; } for (let pubKey of nodesList) { diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index 0535e0881..d80341063 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -141,13 +141,13 @@ export default class CLightningClient extends EventEmitter implements AbstractLi // main data directory provided, default to using the bitcoin mainnet subdirectory // to be removed in v0.2.0 else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { - logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) - logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`) + logger.warn(`${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`, logger.tags.ln) + logger.warn(`specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`, logger.tags.ln) rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') } } - logger.debug(`[CLightningClient] Connecting to ${rpcPath}`); + logger.debug(`Connecting to ${rpcPath}`, logger.tags.ln); super(); this.rpcPath = rpcPath; @@ -172,19 +172,19 @@ export default class CLightningClient extends EventEmitter implements AbstractLi this.clientConnectionPromise = new Promise(resolve => { _self.client.on('connect', () => { - logger.info(`[CLightningClient] Lightning client connected`); + logger.info(`CLightning client connected`, logger.tags.ln); _self.reconnectWait = 1; resolve(); }); _self.client.on('end', () => { - logger.err('[CLightningClient] Lightning client connection closed, reconnecting'); + logger.err(`CLightning client connection closed, reconnecting`, logger.tags.ln); _self.increaseWaitTime(); _self.reconnect(); }); _self.client.on('error', error => { - logger.err(`[CLightningClient] Lightning client connection error: ${error}`); + logger.err(`CLightning client connection error: ${error}`, logger.tags.ln); _self.increaseWaitTime(); _self.reconnect(); }); @@ -196,7 +196,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi return; } const data = JSON.parse(line); - // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); _self.emit('res:' + data.id, data); }); } @@ -217,7 +216,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi } this.reconnectTimeout = setTimeout(() => { - logger.debug('[CLightningClient] Trying to reconnect...'); + logger.debug(`Trying to reconnect...`, logger.tags.ln); _self.client.connect(_self.rpcPath); _self.reconnectTimeout = null; @@ -235,7 +234,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi id: '' + callInt }; - // logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`); // Wait for the client to connect return this.clientConnectionPromise diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 92ae1f0a7..9e36b5c25 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -2,6 +2,7 @@ import { ILightningApi } from '../lightning-api.interface'; import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; import logger from '../../../logger'; import { Common } from '../../common'; +import config from '../../../config'; /** * Convert a clightning "listnode" entry to a lnd node entry @@ -40,7 +41,7 @@ export function convertNode(clNode: any): ILightningApi.Node { * Convert clightning "listchannels" response to lnd "describegraph.edges" format */ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise { - logger.info('Converting clightning nodes and channels to lnd graph format'); + logger.debug(`Converting clightning nodes and channels to lnd graph format`, logger.tags.ln); let loggerTimer = new Date().getTime() / 1000; let channelProcessed = 0; @@ -62,8 +63,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P } const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`, logger.tags.ln); loggerTimer = new Date().getTime() / 1000; } @@ -76,7 +77,7 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id])); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); - if (elapsedSeconds > 10) { + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`); loggerTimer = new Date().getTime() / 1000; } diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index e8ab48230..d94ed77bd 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,17 +1,14 @@ import logger from '../logger'; -import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces'; +import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces'; import { Common } from './common'; import config from '../config'; -import { StaticPool } from 'node-worker-threads-pool'; +import { Worker } from 'worker_threads'; import path from 'path'; class MempoolBlocks { private mempoolBlocks: MempoolBlockWithTransactions[] = []; private mempoolBlockDeltas: MempoolBlockDelta[] = []; - private makeTemplatesPool = new StaticPool({ - size: 1, - task: path.resolve(__dirname, './tx-selection-worker.js'), - }); + private txSelectionWorker: Worker | null = null; constructor() {} @@ -146,27 +143,159 @@ class MempoolBlocks { return mempoolBlockDeltas; } - public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null = null, condenseRest = false): Promise { - const { mempool, blocks } = await this.makeTemplatesPool.exec({ mempool: newMempool, blockLimit, weightLimit, condenseRest }); - const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks); - - // copy CPFP info across to main thread's mempool - Object.keys(newMempool).forEach((txid) => { - if (newMempool[txid] && mempool[txid]) { - newMempool[txid].effectiveFeePerVsize = mempool[txid].effectiveFeePerVsize; - newMempool[txid].ancestors = mempool[txid].ancestors; - newMempool[txid].descendants = mempool[txid].descendants; - newMempool[txid].bestDescendant = mempool[txid].bestDescendant; - newMempool[txid].cpfpChecked = mempool[txid].cpfpChecked; - } + public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }): Promise { + // prepare a stripped down version of the mempool with only the minimum necessary data + // to reduce the overhead of passing this data to the worker thread + const strippedMempool: { [txid: string]: ThreadTransaction } = {}; + Object.values(newMempool).forEach(entry => { + strippedMempool[entry.txid] = { + txid: entry.txid, + fee: entry.fee, + weight: entry.weight, + feePerVsize: entry.fee / (entry.weight / 4), + effectiveFeePerVsize: entry.fee / (entry.weight / 4), + vin: entry.vin.map(v => v.txid), + }; }); - this.mempoolBlocks = blocks; + // (re)initialize tx selection worker thread + if (!this.txSelectionWorker) { + this.txSelectionWorker = new Worker(path.resolve(__dirname, './tx-selection-worker.js')); + // if the thread throws an unexpected error, or exits for any other reason, + // reset worker state so that it will be re-initialized on the next run + this.txSelectionWorker.once('error', () => { + this.txSelectionWorker = null; + }); + this.txSelectionWorker.once('exit', () => { + this.txSelectionWorker = null; + }); + } + + // run the block construction algorithm in a separate thread, and wait for a result + let threadErrorListener; + try { + const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { + threadErrorListener = reject; + this.txSelectionWorker?.once('message', (result): void => { + resolve(result); + }); + this.txSelectionWorker?.once('error', reject); + }); + this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool }); + const { blocks, clusters } = await workerResultPromise; + + this.processBlockTemplates(newMempool, blocks, clusters); + + // clean up thread error listener + this.txSelectionWorker?.removeListener('error', threadErrorListener); + } catch (e) { + logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); + } + } + + public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[]): Promise { + if (!this.txSelectionWorker) { + // need to reset the worker + return this.makeBlockTemplates(newMempool); + } + // prepare a stripped down version of the mempool with only the minimum necessary data + // to reduce the overhead of passing this data to the worker thread + const addedStripped: ThreadTransaction[] = added.map(entry => { + return { + txid: entry.txid, + fee: entry.fee, + weight: entry.weight, + feePerVsize: entry.fee / (entry.weight / 4), + effectiveFeePerVsize: entry.fee / (entry.weight / 4), + vin: entry.vin.map(v => v.txid), + }; + }); + + // run the block construction algorithm in a separate thread, and wait for a result + let threadErrorListener; + try { + const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { + threadErrorListener = reject; + this.txSelectionWorker?.once('message', (result): void => { + resolve(result); + }); + this.txSelectionWorker?.once('error', reject); + }); + this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed }); + const { blocks, clusters } = await workerResultPromise; + + this.processBlockTemplates(newMempool, blocks, clusters); + + // clean up thread error listener + this.txSelectionWorker?.removeListener('error', threadErrorListener); + } catch (e) { + logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); + } + } + + private processBlockTemplates(mempool, blocks, clusters): void { + // update this thread's mempool with the results + blocks.forEach(block => { + block.forEach(tx => { + if (tx.txid in mempool) { + if (tx.effectiveFeePerVsize != null) { + mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize; + } + if (tx.cpfpRoot && tx.cpfpRoot in clusters) { + const ancestors: Ancestor[] = []; + const descendants: Ancestor[] = []; + const cluster = clusters[tx.cpfpRoot]; + let matched = false; + cluster.forEach(txid => { + if (txid === tx.txid) { + matched = true; + } else { + const relative = { + txid: txid, + fee: mempool[txid].fee, + weight: mempool[txid].weight, + }; + if (matched) { + descendants.push(relative); + } else { + ancestors.push(relative); + } + } + }); + mempool[tx.txid].ancestors = ancestors; + mempool[tx.txid].descendants = descendants; + mempool[tx.txid].bestDescendant = null; + } + mempool[tx.txid].cpfpChecked = tx.cpfpChecked; + } + }); + }); + + // unpack the condensed blocks into proper mempool blocks + const mempoolBlocks = blocks.map((transactions, blockIndex) => { + return this.dataToMempoolBlocks(transactions.map(tx => { + return mempool[tx.txid] || null; + }).filter(tx => !!tx), undefined, undefined, blockIndex); + }); + + const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); + + this.mempoolBlocks = mempoolBlocks; this.mempoolBlockDeltas = deltas; } private dataToMempoolBlocks(transactions: TransactionExtended[], - blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions { + blockSize: number | undefined, blockWeight: number | undefined, blocksIndex: number): MempoolBlockWithTransactions { + let totalSize = blockSize || 0; + let totalWeight = blockWeight || 0; + if (blockSize === undefined && blockWeight === undefined) { + totalSize = 0; + totalWeight = 0; + transactions.forEach(tx => { + totalSize += tx.size; + totalWeight += tx.weight; + }); + } let rangeLength = 4; if (blocksIndex === 0) { rangeLength = 8; @@ -177,8 +306,8 @@ class MempoolBlocks { rangeLength = 8; } return { - blockSize: blockSize, - blockVSize: blockWeight / 4, + blockSize: totalSize, + blockVSize: totalWeight / 4, nTx: transactions.length, totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0), medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 584ddf816..717f4eebb 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -21,7 +21,7 @@ class Mempool { private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) | undefined; private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], - deletedTransactions: TransactionExtended[]) => void) | undefined; + deletedTransactions: TransactionExtended[]) => Promise) | undefined; private txPerSecondArray: number[] = []; private txPerSecond: number = 0; diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 55cd33bd3..83e810d43 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -265,9 +265,9 @@ class Mining { } await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate()); if (newlyIndexed > 0) { - logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`); + logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); } else { - logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`); + logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining); } loadingIndicators.setProgress('weekly-hashrate-indexing', 100); } catch (e) { @@ -370,14 +370,14 @@ class Mining { await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate()); if (newlyIndexed > 0) { - logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`); + logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); } else { - logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`); + logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); } loadingIndicators.setProgress('daily-hashrate-indexing', 100); } catch (e) { loadingIndicators.setProgress('daily-hashrate-indexing', 100); - logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); throw e; } } @@ -449,9 +449,9 @@ class Mining { } if (totalIndexed > 0) { - logger.notice(`Indexed ${totalIndexed} difficulty adjustments`); + logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); } else { - logger.debug(`Indexed ${totalIndexed} difficulty adjustments`); + logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); } } diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index ae6bd52ce..c3cc994a2 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -61,7 +61,7 @@ class PoolsParser { poolNames.push(poolsDuplicated[i].name); } } - logger.debug(`Found ${poolNames.length} unique mining pools`); + logger.debug(`Found ${poolNames.length} unique mining pools`, logger.tags.mining); // Get existing pools from the db let existingPools; @@ -72,7 +72,7 @@ class PoolsParser { existingPools = []; } } catch (e) { - logger.err('Cannot get existing pools from the database, skipping pools.json import'); + logger.err('Cannot get existing pools from the database, skipping pools.json import', logger.tags.mining); return; } @@ -99,7 +99,7 @@ class PoolsParser { slug = poolsJson['slugs'][poolNames[i]]; } catch (e) { if (this.slugWarnFlag === false) { - logger.warn(`pools.json does not seem to contain the 'slugs' object`); + logger.warn(`pools.json does not seem to contain the 'slugs' object`, logger.tags.mining); this.slugWarnFlag = true; } } @@ -107,7 +107,7 @@ class PoolsParser { if (slug === undefined) { // Only keep alphanumerical slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase(); - logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`); + logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`, logger.tags.mining); } const poolObj = { @@ -143,9 +143,9 @@ class PoolsParser { 'addresses': allAddresses, 'slug': slug }); - logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`); + logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`, logger.tags.mining); } else { - logger.debug(`Add '${finalPoolName}' mining pool`); + logger.debug(`Add '${finalPoolName}' mining pool`, logger.tags.mining); finalPoolDataAdd.push(poolObj); } } @@ -160,14 +160,14 @@ class PoolsParser { } if (config.DATABASE.ENABLED === false) { // Don't run db operations - logger.info('Mining pools.json import completed (no database)'); + logger.info('Mining pools.json import completed (no database)', logger.tags.mining); return; } if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 || finalPoolDataRename.length > 0 ) { - logger.debug(`Update pools table now`); + logger.debug(`Update pools table now`, logger.tags.mining); // Add new mining pools into the database let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES '; @@ -217,9 +217,9 @@ class PoolsParser { await DB.query({ sql: query, timeout: 120000 }); } await this.insertUnknownPool(); - logger.info('Mining pools.json import completed'); + logger.info('Mining pools.json import completed', logger.tags.mining); } catch (e) { - logger.err(`Cannot import pools in the database`); + logger.err(`Cannot import pools in the database`, logger.tags.mining); throw e; } } @@ -227,7 +227,7 @@ class PoolsParser { try { await this.insertUnknownPool(); } catch (e) { - logger.err(`Cannot insert unknown pool in the database`); + logger.err(`Cannot insert unknown pool in the database`, logger.tags.mining); throw e; } } @@ -252,7 +252,7 @@ class PoolsParser { `); } } catch (e) { - logger.err('Unable to insert "Unknown" mining pool'); + logger.err('Unable to insert "Unknown" mining pool', logger.tags.mining); } } @@ -272,17 +272,17 @@ class PoolsParser { for (const updatedPool of finalPoolDataUpdate) { const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`); if (pool.length > 0) { - logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`); + logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`, logger.tags.mining); await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`); } } // Ignore early days of Bitcoin as there were not mining pool yet - logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing'); + logger.notice(`Deleting blocks with unknown mining pool from height 130635 for future re-indexing`, logger.tags.mining); const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`); await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`); - logger.notice('Truncating hashrates for future re-indexing'); + logger.notice(`Truncating hashrates for future re-indexing`, logger.tags.mining); await DB.query(`DELETE FROM hashrates`); } } diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 5b92cea5f..fb5aeea42 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -1,8 +1,7 @@ -import bitcoinApi from './bitcoin/bitcoin-api-factory'; import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces'; import { IEsploraApi } from './bitcoin/esplora-api.interface'; -import config from '../config'; import { Common } from './common'; +import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; class TransactionUtils { constructor() { } @@ -21,8 +20,19 @@ class TransactionUtils { }; } - public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise { - const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts); + /** + * @param txId + * @param addPrevouts + * @param lazyPrevouts + * @param forceCore - See https://github.com/mempool/mempool/issues/2904 + */ + public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise { + let transaction: IEsploraApi.Transaction; + if (forceCore === true) { + transaction = await bitcoinCoreApi.$getRawTransaction(txId, true); + } else { + transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts); + } return this.extendTransaction(transaction); } diff --git a/backend/src/api/tx-selection-worker.ts b/backend/src/api/tx-selection-worker.ts index ca40af84f..7297cbe88 100644 --- a/backend/src/api/tx-selection-worker.ts +++ b/backend/src/api/tx-selection-worker.ts @@ -1,17 +1,30 @@ import config from '../config'; import logger from '../logger'; -import { TransactionExtended, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces'; +import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces'; import { PairingHeap } from '../utils/pairing-heap'; import { Common } from './common'; import { parentPort } from 'worker_threads'; +let mempool: { [txid: string]: ThreadTransaction } = {}; + if (parentPort) { - parentPort.on('message', (params: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit: number | null, condenseRest: boolean}) => { - const { mempool, blocks } = makeBlockTemplates(params); + parentPort.on('message', (params) => { + if (params.type === 'set') { + mempool = params.mempool; + } else if (params.type === 'update') { + params.added.forEach(tx => { + mempool[tx.txid] = tx; + }); + params.removed.forEach(txid => { + delete mempool[txid]; + }); + } + + const { blocks, clusters } = makeBlockTemplates(mempool); // return the result to main thread. if (parentPort) { - parentPort.postMessage({ mempool, blocks }); + parentPort.postMessage({ blocks, clusters }); } }); } @@ -19,35 +32,24 @@ if (parentPort) { /* * 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. */ -function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: { mempool: { [txid: string]: TransactionExtended }, blockLimit: number, weightLimit?: number | null, condenseRest?: boolean | null }) - : { mempool: { [txid: string]: TransactionExtended }, blocks: MempoolBlockWithTransactions[] } { +function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction }) + : { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } { const start = Date.now(); const auditPool: { [txid: string]: AuditTransaction } = {}; const mempoolArray: AuditTransaction[] = []; - const restOfArray: TransactionExtended[] = []; + const restOfArray: ThreadTransaction[] = []; + const cpfpClusters: { [root: string]: string[] } = {}; - 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, + effectiveFeePerVsize: tx.feePerVsize, vin: tx.vin, relativesSet: false, ancestorMap: new Map(), @@ -74,7 +76,7 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: // 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[] = []; + const blocks: ThreadTransaction[][] = []; let blockWeight = 4000; let blockSize = 0; let transactions: AuditTransaction[] = []; @@ -82,7 +84,7 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: let overflow: AuditTransaction[] = []; let failures = 0; let top = 0; - while ((top < mempoolArray.length || !modified.isEmpty()) && (condenseRest || blocks.length < blockLimit)) { + while ((top < mempoolArray.length || !modified.isEmpty())) { // skip invalid transactions while (top < mempoolArray.length && (mempoolArray[top].used || mempoolArray[top].modified)) { top++; @@ -107,9 +109,13 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: // Check if the package fits into this block if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) { const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values()); - const descendants: AuditTransaction[] = []; // 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]; + let isCluster = false; + if (sortedTxSet.length > 1) { + cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid); + isCluster = true; + } const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); const used: AuditTransaction[] = []; while (sortedTxSet.length) { @@ -119,21 +125,9 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: ancestor.usedBy = nextTx.txid; // update original copy of this tx with effective fee rate & relatives data mempoolTx.effectiveFeePerVsize = effectiveFeeRate; - mempoolTx.ancestors = sortedTxSet.map((a) => { - return { - txid: a.txid, - fee: a.fee, - weight: a.weight, - }; - }).reverse(); - mempoolTx.descendants = descendants.map((a) => { - return { - txid: a.txid, - fee: a.fee, - weight: a.weight, - }; - }); - descendants.push(ancestor); + if (isCluster) { + mempoolTx.cpfpRoot = nextTx.txid; + } mempoolTx.cpfpChecked = true; transactions.push(ancestor); blockSize += ancestor.size; @@ -159,10 +153,10 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: // this block is full const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000); const queueEmpty = top >= mempoolArray.length && modified.isEmpty(); - if ((exceededPackageTries || queueEmpty) && (!condenseRest || blocks.length < blockLimit - 1)) { + if ((exceededPackageTries || queueEmpty) && blocks.length < 7) { // construct this block if (transactions.length) { - blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length)); + blocks.push(transactions.map(t => mempool[t.txid])); } // reset for the next block transactions = []; @@ -181,55 +175,40 @@ function makeBlockTemplates({ mempool, blockLimit, weightLimit, condenseRest }: 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; - const mempoolTx = mempool[tx.txid]; - // update original copy of this tx with effective fee rate & relatives data - mempoolTx.effectiveFeePerVsize = tx.score; - mempoolTx.ancestors = (Array.from(tx.ancestorMap?.values()) as AuditTransaction[]).map((a) => { - return { - txid: a.txid, - fee: a.fee, - weight: a.weight, - }; - }); - mempoolTx.bestDescendant = null; - mempoolTx.cpfpChecked = true; - transactions.push(tx); - tx.used = true; + // pack any leftover transactions into the last block + for (const tx of overflow) { + if (!tx || tx?.used) { + continue; } - const blockTransactions = transactions.map(t => mempool[t.txid]); - restOfArray.forEach(tx => { - blockWeight += tx.weight; - blockSize += tx.size; - tx.effectiveFeePerVsize = tx.feePerVsize; - tx.cpfpChecked = false; - tx.ancestors = []; - tx.bestDescendant = null; - blockTransactions.push(tx); - }); - if (blockTransactions.length) { - blocks.push(dataToMempoolBlocks(blockTransactions, blockSize, blockWeight, blocks.length)); + blockWeight += tx.weight; + const mempoolTx = mempool[tx.txid]; + // update original copy of this tx with effective fee rate & relatives data + mempoolTx.effectiveFeePerVsize = tx.score; + if (tx.ancestorMap.size > 0) { + cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid); + mempoolTx.cpfpRoot = tx.txid; } - transactions = []; - } else if (transactions.length) { - blocks.push(dataToMempoolBlocks(transactions.map(t => mempool[t.txid]), blockSize, blockWeight, blocks.length)); + mempoolTx.cpfpChecked = true; + transactions.push(tx); + tx.used = true; } + const blockTransactions = transactions.map(t => mempool[t.txid]); + restOfArray.forEach(tx => { + blockWeight += tx.weight; + tx.effectiveFeePerVsize = tx.feePerVsize; + tx.cpfpChecked = false; + blockTransactions.push(tx); + }); + if (blockTransactions.length) { + blocks.push(blockTransactions); + } + transactions = []; const end = Date.now(); const time = end - start; logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds'); - return { - mempool, - blocks - }; + return { blocks, clusters: cpfpClusters }; } // traverse in-mempool ancestors @@ -239,9 +218,9 @@ function setRelatives( 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); + const parentTx = mempool[parent]; + if (parentTx && !tx.ancestorMap?.has(parent)) { + tx.ancestorMap.set(parent, parentTx); parentTx.children.add(tx); // visit each node only once if (!parentTx.relativesSet) { @@ -312,27 +291,4 @@ function updateDescendants( }); } } -} - -function dataToMempoolBlocks(transactions: TransactionExtended[], - blockSize: number, blockWeight: number, blocksIndex: number): MempoolBlockWithTransactions { - let rangeLength = 4; - if (blocksIndex === 0) { - rangeLength = 8; - } - if (transactions.length > 4000) { - rangeLength = 6; - } else if (transactions.length > 10000) { - rangeLength = 8; - } - return { - blockSize: blockSize, - blockVSize: blockWeight / 4, - nTx: transactions.length, - totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0), - medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), - feeRange: Common.getFeesInRange(transactions, rangeLength), - transactionIds: transactions.map((tx) => tx.txid), - transactions: transactions.map((tx) => Common.stripTransaction(tx)), - }; } \ No newline at end of file diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index c78c93544..b6f32aa05 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -251,7 +251,7 @@ class WebsocketHandler { } if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { - await mempoolBlocks.makeBlockTemplates(newMempool, 8, null, true); + await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid)); } else { mempoolBlocks.updateMempoolBlocks(newMempool); } @@ -419,7 +419,7 @@ class WebsocketHandler { const _memPool = memPool.getMempool(); if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { - await mempoolBlocks.makeBlockTemplates(_memPool, 2); + await mempoolBlocks.makeBlockTemplates(_memPool); } else { mempoolBlocks.updateMempoolBlocks(_memPool); } @@ -462,13 +462,15 @@ class WebsocketHandler { } } + const removed: string[] = []; // Update mempool to remove transactions included in the new block for (const txId of txIds) { delete _memPool[txId]; + removed.push(txId); } if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { - await mempoolBlocks.makeBlockTemplates(_memPool, 8, null, true); + await mempoolBlocks.updateBlockTemplates(_memPool, [], removed); } else { mempoolBlocks.updateMempoolBlocks(_memPool); } diff --git a/backend/src/config.ts b/backend/src/config.ts index e97deb5e5..fb06c84fb 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -31,7 +31,7 @@ interface IConfig { POOLS_JSON_TREE_URL: string, ADVANCED_GBT_AUDIT: boolean; ADVANCED_GBT_MEMPOOL: boolean; - TRANSACTION_INDEXING: boolean; + CPFP_INDEXING: boolean; }; ESPLORA: { REST_API_URL: string; @@ -152,7 +152,7 @@ const defaults: IConfig = { 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', 'ADVANCED_GBT_AUDIT': false, 'ADVANCED_GBT_MEMPOOL': false, - 'TRANSACTION_INDEXING': false, + 'CPFP_INDEXING': false, }, 'ESPLORA': { 'REST_API_URL': 'http://127.0.0.1:3000', diff --git a/backend/src/logger.ts b/backend/src/logger.ts index 63774d513..634f8f582 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -32,22 +32,27 @@ class Logger { local7: 23 }; + public tags = { + mining: 'Mining', + ln: 'Lightning', + }; + // @ts-ignore - public emerg: ((msg: string) => void); + public emerg: ((msg: string, tag?: string) => void); // @ts-ignore - public alert: ((msg: string) => void); + public alert: ((msg: string, tag?: string) => void); // @ts-ignore - public crit: ((msg: string) => void); + public crit: ((msg: string, tag?: string) => void); // @ts-ignore - public err: ((msg: string) => void); + public err: ((msg: string, tag?: string) => void); // @ts-ignore - public warn: ((msg: string) => void); + public warn: ((msg: string, tag?: string) => void); // @ts-ignore - public notice: ((msg: string) => void); + public notice: ((msg: string, tag?: string) => void); // @ts-ignore - public info: ((msg: string) => void); + public info: ((msg: string, tag?: string) => void); // @ts-ignore - public debug: ((msg: string) => void); + public debug: ((msg: string, tag?: string) => void); private name = 'mempool'; private client: dgram.Socket; @@ -66,8 +71,8 @@ class Logger { private addprio(prio): void { this[prio] = (function(_this) { - return function(msg) { - return _this.msg(prio, msg); + return function(msg, tag?: string) { + return _this.msg(prio, msg, tag); }; })(this); } @@ -85,7 +90,7 @@ class Logger { return ''; } - private msg(priority, msg) { + private msg(priority, msg, tag?: string) { let consolemsg, prionum, syslogmsg; if (typeof msg === 'string' && msg.length > 0) { while (msg[msg.length - 1].charCodeAt(0) === 10) { @@ -94,10 +99,10 @@ class Logger { } const network = this.network ? ' <' + this.network + '>' : ''; prionum = Logger.priorities[priority] || Logger.priorities.info; - consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`; + consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${tag ? '[' + tag + '] ' : ''}${msg}`; if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) { - syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`; + syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${tag ? '[' + tag + '] ' : ''}${msg}`; this.syslog(syslogmsg); } if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 11de304b8..f79786279 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -81,10 +81,10 @@ export interface TransactionExtended extends IEsploraApi.Transaction { export interface AuditTransaction { txid: string; fee: number; - size: number; weight: number; feePerVsize: number; - vin: IEsploraApi.Vin[]; + effectiveFeePerVsize: number; + vin: string[]; relativesSet: boolean; ancestorMap: Map; children: Set; @@ -96,6 +96,17 @@ export interface AuditTransaction { modifiedNode: HeapNode; } +export interface ThreadTransaction { + txid: string; + fee: number; + weight: number; + feePerVsize: number; + effectiveFeePerVsize?: number; + vin: string[]; + cpfpRoot?: string; + cpfpChecked?: boolean; +} + export interface Ancestor { txid: string; weight: number; @@ -263,6 +274,7 @@ export interface IBackendInfo { hostname: string; gitCommit: string; version: string; + lightning: boolean; } export interface IDifficultyAdjustment { @@ -326,4 +338,4 @@ export interface IOldestNodes { updatedAt?: number, city?: any, country?: any, -} \ No newline at end of file +} diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 78a8fcce2..df98719b9 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -8,6 +8,8 @@ import HashratesRepository from './HashratesRepository'; import { escape } from 'mysql2'; import BlocksSummariesRepository from './BlocksSummariesRepository'; import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; +import bitcoinClient from '../api/bitcoin/bitcoin-client'; +import config from '../config'; class BlocksRepository { /** @@ -667,16 +669,32 @@ class BlocksRepository { */ public async $getCPFPUnindexedBlocks(): Promise { try { - const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`); - return rows; + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); + const currentBlockHeight = blockchainInfo.blocks; + let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight); + if (indexingBlockAmount <= -1) { + indexingBlockAmount = currentBlockHeight + 1; + } + const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); + + const [rows]: any[] = await DB.query(` + SELECT height + FROM compact_cpfp_clusters + WHERE height <= ? AND height >= ? + ORDER BY height DESC; + `, [currentBlockHeight, minHeight]); + + const indexedHeights = {}; + rows.forEach((row) => { indexedHeights[row.height] = true; }); + const allHeights: number[] = Array.from(Array(currentBlockHeight - minHeight + 1).keys(), n => n + minHeight).reverse(); + const unindexedHeights = allHeights.filter(x => !indexedHeights[x]); + + return unindexedHeights; } catch (e) { logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e)); throw e; } - } - - public async $setCPFPIndexed(hash: string): Promise { - await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]); + return []; } /** diff --git a/backend/src/repositories/CpfpRepository.ts b/backend/src/repositories/CpfpRepository.ts index 563e6ede1..ce7432d5b 100644 --- a/backend/src/repositories/CpfpRepository.ts +++ b/backend/src/repositories/CpfpRepository.ts @@ -1,34 +1,151 @@ +import cluster, { Cluster } from 'cluster'; +import { RowDataPacket } from 'mysql2'; import DB from '../database'; import logger from '../logger'; import { Ancestor } from '../mempool.interfaces'; +import transactionRepository from '../repositories/TransactionRepository'; class CpfpRepository { - public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise { + public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise { + if (!txs[0]) { + return false; + } + // skip clusters of transactions with the same fees + const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100; + const equalFee = txs.reduce((acc, tx) => { + return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); + }, true); + if (equalFee) { + return false; + } + try { - const txsJson = JSON.stringify(txs); + const packedTxs = Buffer.from(this.pack(txs)); await DB.query( ` - INSERT INTO cpfp_clusters(root, height, txs, fee_rate) - VALUE (?, ?, ?, ?) + INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate) + VALUE (UNHEX(?), ?, ?, ?) ON DUPLICATE KEY UPDATE height = ?, txs = ?, fee_rate = ? `, - [txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height] + [clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize] ); + const maxChunk = 10; + let chunkIndex = 0; + while (chunkIndex < txs.length) { + const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => { + return { txid: tx.txid, cluster: clusterRoot }; + }); + await transactionRepository.$batchSetCluster(chunk); + chunkIndex += maxChunk; + } + return true; } catch (e: any) { logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e)); throw e; } } + public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise { + try { + const clusterValues: any[] = []; + const txs: any[] = []; + + for (const cluster of clusters) { + if (cluster.txs?.length > 1) { + const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100; + const equalFee = cluster.txs.reduce((acc, tx) => { + return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee); + }, true); + if (!equalFee) { + clusterValues.push([ + cluster.root, + cluster.height, + Buffer.from(this.pack(cluster.txs)), + cluster.effectiveFeePerVsize + ]); + for (const tx of cluster.txs) { + txs.push({ txid: tx.txid, cluster: cluster.root }); + } + } + } + } + + if (!clusterValues.length) { + return false; + } + + const maxChunk = 100; + let chunkIndex = 0; + // insert transactions in batches of up to 100 rows + while (chunkIndex < txs.length) { + const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk); + await transactionRepository.$batchSetCluster(chunk); + chunkIndex += maxChunk; + } + + chunkIndex = 0; + // insert clusters in batches of up to 100 rows + while (chunkIndex < clusterValues.length) { + const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk); + let query = ` + INSERT IGNORE INTO compact_cpfp_clusters(root, height, txs, fee_rate) + VALUES + `; + query += chunk.map(chunk => { + return (' (UNHEX(?), ?, ?, ?)'); + }) + ';'; + const values = chunk.flat(); + await DB.query( + query, + values + ); + chunkIndex += maxChunk; + } + return true; + } catch (e: any) { + logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getCluster(clusterRoot: string): Promise { + const [clusterRows]: any = await DB.query( + ` + SELECT * + FROM compact_cpfp_clusters + WHERE root = UNHEX(?) + `, + [clusterRoot] + ); + const cluster = clusterRows[0]; + cluster.txs = this.unpack(cluster.txs); + return cluster; + } + public async $deleteClustersFrom(height: number): Promise { logger.info(`Delete newer cpfp clusters from height ${height} from the database`); try { + const [rows] = await DB.query( + ` + SELECT txs, height, root from compact_cpfp_clusters + WHERE height >= ? + `, + [height] + ) as RowDataPacket[][]; + if (rows?.length) { + for (let clusterToDelete of rows) { + const txs = this.unpack(clusterToDelete.txs); + for (let tx of txs) { + await transactionRepository.$removeTransaction(tx.txid); + } + } + } await DB.query( ` - DELETE from cpfp_clusters + DELETE from compact_cpfp_clusters WHERE height >= ? `, [height] @@ -38,6 +155,70 @@ class CpfpRepository { throw e; } } + + // insert a dummy row to mark that we've indexed as far as this block + public async $insertProgressMarker(height: number): Promise { + try { + const [rows]: any = await DB.query( + ` + SELECT root + FROM compact_cpfp_clusters + WHERE height = ? + `, + [height] + ); + if (!rows?.length) { + const rootBuffer = Buffer.alloc(32); + rootBuffer.writeInt32LE(height); + await DB.query( + ` + INSERT INTO compact_cpfp_clusters(root, height, fee_rate) + VALUE (?, ?, ?) + `, + [rootBuffer, height, 0] + ); + } + } catch (e: any) { + logger.err(`Cannot insert cpfp progress marker. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public pack(txs: Ancestor[]): ArrayBuffer { + const buf = new ArrayBuffer(44 * txs.length); + const view = new DataView(buf); + txs.forEach((tx, i) => { + const offset = i * 44; + for (let x = 0; x < 32; x++) { + // store txid in little-endian + view.setUint8(offset + (31 - x), parseInt(tx.txid.slice(x * 2, (x * 2) + 2), 16)); + } + view.setUint32(offset + 32, tx.weight); + view.setBigUint64(offset + 36, BigInt(Math.round(tx.fee))); + }); + return buf; + } + + public unpack(buf: Buffer): Ancestor[] { + if (!buf) { + return []; + } + + const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + const txs: Ancestor[] = []; + const view = new DataView(arrayBuffer); + for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) { + const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join(''); + const weight = view.getUint32(offset + 32); + const fee = Number(view.getBigUint64(offset + 36)); + txs.push({ + txid, + weight, + fee + }); + } + return txs; + } } export default new CpfpRepository(); \ No newline at end of file diff --git a/backend/src/repositories/TransactionRepository.ts b/backend/src/repositories/TransactionRepository.ts index 1c6e3719f..061617451 100644 --- a/backend/src/repositories/TransactionRepository.ts +++ b/backend/src/repositories/TransactionRepository.ts @@ -1,6 +1,7 @@ import DB from '../database'; import logger from '../logger'; import { Ancestor, CpfpInfo } from '../mempool.interfaces'; +import cpfpRepository from './CpfpRepository'; interface CpfpSummary { txid: string; @@ -12,20 +13,20 @@ interface CpfpSummary { } class TransactionRepository { - public async $setCluster(txid: string, cluster: string): Promise { + public async $setCluster(txid: string, clusterRoot: string): Promise { try { await DB.query( ` - INSERT INTO transactions + INSERT INTO compact_transactions ( txid, cluster ) - VALUE (?, ?) + VALUE (UNHEX(?), UNHEX(?)) ON DUPLICATE KEY UPDATE - cluster = ? + cluster = UNHEX(?) ;`, - [txid, cluster, cluster] + [txid, clusterRoot, clusterRoot] ); } catch (e: any) { logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e)); @@ -33,18 +34,45 @@ class TransactionRepository { } } - public async $getCpfpInfo(txid: string): Promise { + public async $batchSetCluster(txs): Promise { try { let query = ` - SELECT * - FROM transactions - LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster - WHERE transactions.txid = ? + INSERT IGNORE INTO compact_transactions + ( + txid, + cluster + ) + VALUES `; - const [rows]: any = await DB.query(query, [txid]); - if (rows.length) { - rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[]; - return this.convertCpfp(rows[0]); + query += txs.map(tx => { + return (' (UNHEX(?), UNHEX(?))'); + }) + ';'; + const values = txs.map(tx => [tx.txid, tx.cluster]).flat(); + await DB.query( + query, + values + ); + } catch (e: any) { + logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getCpfpInfo(txid: string): Promise { + try { + const [txRows]: any = await DB.query( + ` + SELECT HEX(txid) as id, HEX(cluster) as root + FROM compact_transactions + WHERE txid = UNHEX(?) + `, + [txid] + ); + if (txRows.length && txRows[0].root != null) { + const txid = txRows[0].id.toLowerCase(); + const clusterId = txRows[0].root.toLowerCase(); + const cluster = await cpfpRepository.$getCluster(clusterId); + return this.convertCpfp(txid, cluster); } } catch (e) { logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e)); @@ -52,12 +80,23 @@ class TransactionRepository { } } - private convertCpfp(cpfp: CpfpSummary): CpfpInfo { + public async $removeTransaction(txid: string): Promise { + await DB.query( + ` + DELETE FROM compact_transactions + WHERE txid = UNHEX(?) + `, + [txid] + ); + } + + private convertCpfp(txid, cluster): CpfpInfo { const descendants: Ancestor[] = []; const ancestors: Ancestor[] = []; let matched = false; - for (const tx of cpfp.txs) { - if (tx.txid === cpfp.txid) { + + for (const tx of cluster.txs) { + if (tx.txid === txid) { matched = true; } else if (!matched) { descendants.push(tx); @@ -68,7 +107,6 @@ class TransactionRepository { return { descendants, ancestors, - effectiveFeePerVsize: cpfp.fee_rate }; } } diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index c5e5a102d..46f51e7bd 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -23,7 +23,7 @@ class NetworkSyncService { constructor() {} public async $startService(): Promise { - logger.info('Starting lightning network sync service'); + logger.info(`Starting lightning network sync service`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; @@ -33,11 +33,11 @@ class NetworkSyncService { private async $runTasks(): Promise { const taskStartTime = Date.now(); try { - logger.info(`Updating nodes and channels`); + logger.debug(`Updating nodes and channels`, logger.tags.ln); const networkGraph = await lightningApi.$getNetworkGraph(); if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) { - logger.info(`LN Network graph is empty, retrying in 10 seconds`); + logger.info(`LN Network graph is empty, retrying in 10 seconds`, logger.tags.ln); setTimeout(() => { this.$runTasks(); }, 10000); return; } @@ -55,7 +55,7 @@ class NetworkSyncService { } } catch (e) { - logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e)); + logger.err(`$runTasks() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime))); @@ -79,8 +79,8 @@ class NetworkSyncService { ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Updating node ${progress}/${nodes.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.debug(`Updating node ${progress}/${nodes.length}`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; } @@ -106,7 +106,7 @@ class NetworkSyncService { deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes); } } - logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`); + logger.debug(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`); // If a channel if not present in the graph, mark it as inactive await nodesApi.$setNodesInactive(graphNodesPubkeys); @@ -138,18 +138,18 @@ class NetworkSyncService { ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Updating channel ${progress}/${channels.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.debug(`Updating channel ${progress}/${channels.length}`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; } } - logger.info(`${progress} channels updated`); + logger.debug(`${progress} channels updated`, logger.tags.ln); // If a channel if not present in the graph, mark it as inactive await channelsApi.$setChannelsInactive(graphChannelsIds); } catch (e) { - logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.err(` Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.ln); } } @@ -184,26 +184,28 @@ class NetworkSyncService { if (lowest < node.first_seen) { const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`; const params = [lowest, node.public_key]; + ++updated; await DB.query(query, params); } ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Updating node first seen date ${progress}/${nodes.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.debug(`Updating node first seen date ${progress}/${nodes.length}`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; - ++updated; } } - logger.info(`Updated ${updated} node first seen dates`); + if (updated > 0) { + logger.debug(`Updated ${updated} node first seen dates`, logger.tags.ln); + } } catch (e) { - logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e)); + logger.err(`$updateNodeFirstSeen() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } } private async $lookUpCreationDateFromChain(): Promise { let progress = 0; - logger.info(`Running channel creation date lookup`); + logger.debug(`Running channel creation date lookup`, logger.tags.ln); try { const channels = await channelsApi.$getChannelsWithoutCreatedDate(); for (const channel of channels) { @@ -214,14 +216,17 @@ class NetworkSyncService { ); ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Updating channel creation date ${progress}/${channels.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.debug(`Updating channel creation date ${progress}/${channels.length}`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; } } - logger.info(`Updated ${channels.length} channels' creation date`); + + if (channels.length > 0) { + logger.debug(`Updated ${channels.length} channels' creation date`, logger.tags.ln); + } } catch (e) { - logger.err('$lookUpCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e)); + logger.err(`$lookUpCreationDateFromChain() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } } @@ -230,7 +235,7 @@ class NetworkSyncService { * mark that channel as inactive */ private async $deactivateChannelsWithoutActiveNodes(): Promise { - logger.info(`Find channels which nodes are offline`); + logger.debug(`Find channels which nodes are offline`, logger.tags.ln); try { const result = await DB.query(` @@ -253,12 +258,10 @@ class NetworkSyncService { `); if (result[0].changedRows ?? 0 > 0) { - logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`); - } else { - logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`); + logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`, logger.tags.ln); } } catch (e) { - logger.err('$deactivateChannelsWithoutActiveNodes() error: ' + (e instanceof Error ? e.message : e)); + logger.err(`$deactivateChannelsWithoutActiveNodes() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } } @@ -277,13 +280,13 @@ class NetworkSyncService { } else { log += ` for the first time`; } - logger.info(log); + logger.info(`${log}`, logger.tags.ln); const channels = await channelsApi.$getChannelsByStatus([0, 1]); for (const channel of channels) { const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); if (spendingTx.spent === true && spendingTx.status?.confirmed === true) { - logger.debug('Marking channel: ' + channel.id + ' as closed.'); + logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln); await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`, [spendingTx.status.block_time, channel.id]); if (spendingTx.txid && !channel.closing_transaction_id) { @@ -293,16 +296,16 @@ class NetworkSyncService { ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Checking if channel has been closed ${progress}/${channels.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln); this.loggerTimer = new Date().getTime() / 1000; } } this.closedChannelsScanBlock = blocks.getCurrentBlockHeight(); - logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`); + logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln); } catch (e) { - logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); + logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } } } diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 9e6e5bd82..69b4cbadd 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -6,7 +6,7 @@ import { Common } from '../../api/common'; class LightningStatsUpdater { public async $startService(): Promise { - logger.info('Starting Lightning Stats service'); + logger.info(`Starting Lightning Stats service`, logger.tags.ln); await this.$runTasks(); LightningStatsImporter.$run(); @@ -27,7 +27,7 @@ class LightningStatsUpdater { const networkGraph = await lightningApi.$getNetworkGraph(); await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); - logger.info(`Updated latest network stats`); + logger.debug(`Updated latest network stats`, logger.tags.ln); } } diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index 76865dc40..c37d96980 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -21,10 +21,10 @@ class FundingTxFetcher { try { this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8')); } catch (e) { - logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`); + logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`, logger.tags.ln); this.fundingTxCache = {}; } - logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`); + logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`, logger.tags.ln); } } @@ -44,26 +44,27 @@ class FundingTxFetcher { ++channelProcessed; let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); - if (elapsedSeconds > 10) { + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer); logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + `(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` + - `elapsed: ${elapsedSeconds} seconds` + `elapsed: ${elapsedSeconds} seconds`, + logger.tags.ln ); loggerTimer = new Date().getTime() / 1000; } elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer); if (elapsedSeconds > 60) { - logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); + logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); cacheTimer = new Date().getTime() / 1000; } } if (this.channelNewlyProcessed > 0) { - logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`); - logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); + logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`, logger.tags.ln); + logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln); fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); } diff --git a/backend/src/tasks/lightning/sync-tasks/node-locations.ts b/backend/src/tasks/lightning/sync-tasks/node-locations.ts index afd280ec7..17974275c 100644 --- a/backend/src/tasks/lightning/sync-tasks/node-locations.ts +++ b/backend/src/tasks/lightning/sync-tasks/node-locations.ts @@ -14,7 +14,7 @@ export async function $lookupNodeLocation(): Promise { let nodesUpdated = 0; let geoNamesInserted = 0; - logger.info(`Running node location updater using Maxmind`); + logger.debug(`Running node location updater using Maxmind`, logger.tags.ln); try { const nodes = await nodesApi.$getAllNodes(); const lookupCity = await maxmind.open(config.MAXMIND.GEOLITE2_CITY); @@ -152,8 +152,8 @@ export async function $lookupNodeLocation(): Promise { ++progress; const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); - if (elapsedSeconds > 10) { - logger.info(`Updating node location data ${progress}/${nodes.length}`); + if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { + logger.debug(`Updating node location data ${progress}/${nodes.length}`); loggerTimer = new Date().getTime() / 1000; } } @@ -161,9 +161,7 @@ export async function $lookupNodeLocation(): Promise { } 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`); + logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`, logger.tags.ln); } } 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 6d6c9e4d3..14f592a14 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -8,7 +8,6 @@ import { isIP } from 'net'; import { Common } from '../../../api/common'; import channelsApi from '../../../api/explorer/channels.api'; import nodesApi from '../../../api/explorer/nodes.api'; -import { ResultSetHeader } from 'mysql2'; const fsPromises = promises; @@ -17,7 +16,7 @@ class LightningStatsImporter { async $run(): Promise { const [channels]: any[] = await DB.query('SELECT short_id from channels;'); - logger.info('Caching funding txs for currently existing channels'); + logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln); await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) { @@ -108,7 +107,7 @@ class LightningStatsImporter { const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id); if (!tx) { - logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`); + logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`, logger.tags.ln); continue; } @@ -310,13 +309,18 @@ class LightningStatsImporter { * Import topology files LN historical data into the database */ async $importHistoricalLightningStats(): Promise { + if (!config.LIGHTNING.TOPOLOGY_FOLDER) { + logger.info(`Lightning topology folder is not set. Not importing historical LN stats`); + return; + } + logger.debug('Run the historical importer'); try { let fileList: string[] = []; try { fileList = await fsPromises.readdir(this.topologiesFolder); } catch (e) { - logger.err(`Unable to open topology folder at ${this.topologiesFolder}`); + logger.err(`Unable to open topology folder at ${this.topologiesFolder}`, logger.tags.ln); throw e; } // Insert history from the most recent to the oldest @@ -354,7 +358,7 @@ class LightningStatsImporter { continue; } - logger.debug(`Reading ${this.topologiesFolder}/${filename}`); + logger.debug(`Reading ${this.topologiesFolder}/${filename}`, logger.tags.ln); let fileContent = ''; try { fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); @@ -363,7 +367,7 @@ class LightningStatsImporter { totalProcessed++; continue; } - logger.err(`Unable to open ${this.topologiesFolder}/${filename}`); + logger.err(`Unable to open ${this.topologiesFolder}/${filename}`, logger.tags.ln); totalProcessed++; continue; } @@ -373,7 +377,7 @@ class LightningStatsImporter { graph = JSON.parse(fileContent); 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}`); + logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); totalProcessed++; continue; } @@ -385,20 +389,20 @@ class LightningStatsImporter { } if (!logStarted) { - logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`); + logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`, logger.tags.ln); logStarted = true; } const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; - logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`); + logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`, logger.tags.ln); totalProcessed++; if (processed > 10) { - logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); + logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln); processed = 0; } else { - logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); + logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln); } await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2))); const stat = await this.computeNetworkStats(timestamp, graph, true); @@ -407,10 +411,10 @@ class LightningStatsImporter { } if (totalProcessed > 0) { - logger.info(`Lightning network stats historical import completed`); + logger.notice(`Lightning network stats historical import completed`, logger.tags.ln); } } catch (e) { - logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`); + logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); } } diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 11bb8060f..086a00cea 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -32,9 +32,9 @@ class PoolsUpdater { this.lastRun = now; if (config.SOCKS5PROXY.ENABLED) { - logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`); + logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`, logger.tags.mining); } else { - logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`); + logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`, logger.tags.mining); } try { @@ -53,9 +53,9 @@ class PoolsUpdater { } if (this.currentSha === undefined) { - logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`); + logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`, logger.tags.mining); } else { - logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`); + logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`, logger.tags.mining); } const poolsJson = await this.query(this.poolsUrl); if (poolsJson === undefined) { @@ -63,11 +63,11 @@ class PoolsUpdater { } await poolsParser.migratePoolsJson(poolsJson); await this.updateDBSha(githubSha); - logger.notice('PoolsUpdater completed'); + logger.notice(`PoolsUpdater completed`, logger.tags.mining); } catch (e) { this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week - logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err(`PoolsUpdater failed. Will try again in 24h. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); } } @@ -81,7 +81,7 @@ class PoolsUpdater { await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); } catch (e) { - logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); } } } @@ -94,7 +94,7 @@ class PoolsUpdater { const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); return (rows.length > 0 ? rows[0].string : undefined); } catch (e) { - logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); return undefined; } } @@ -113,7 +113,7 @@ class PoolsUpdater { } } - logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`); + logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`, logger.tags.mining); return undefined; } diff --git a/backend/src/tasks/price-feeds/kraken-api.ts b/backend/src/tasks/price-feeds/kraken-api.ts index ddb3c4f65..ea02a772d 100644 --- a/backend/src/tasks/price-feeds/kraken-api.ts +++ b/backend/src/tasks/price-feeds/kraken-api.ts @@ -91,7 +91,7 @@ class KrakenApi implements PriceFeed { } if (Object.keys(priceHistory).length > 0) { - logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`); + logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); } } } diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index e069e4db4..9e7e5910a 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -82,7 +82,7 @@ class PriceUpdater { await this.$updatePrice(); } } catch (e) { - logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`); + logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); } this.running = false; @@ -115,14 +115,14 @@ class PriceUpdater { if (price > 0) { prices.push(price); } - logger.debug(`${feed.name} BTC/${currency} price: ${price}`); + logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining); } catch (e) { - logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`); + logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); } } } if (prices.length === 1) { - logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`); + logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`, logger.tags.mining); } // Compute average price, non weighted @@ -175,9 +175,9 @@ class PriceUpdater { ++insertedCount; } if (insertedCount > 0) { - logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`); + logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining); } else { - logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`); + logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining); } // Insert Kraken weekly prices @@ -198,7 +198,7 @@ class PriceUpdater { private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise { const existingPriceTimes = await PricesRepository.$getPricesTimes(); - logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database, this may take a while`); + logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining); const historicalPrices: PriceHistory[] = []; @@ -207,7 +207,7 @@ class PriceUpdater { try { historicalPrices.push(await feed.$fetchRecentPrice(this.currencies, type)); } catch (e) { - logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`); + logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); } } @@ -252,9 +252,9 @@ class PriceUpdater { } if (totalInserted > 0) { - logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`); + logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining); } else { - logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`); + logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining); } } } diff --git a/docker/README.md b/docker/README.md index 4061f420c..0c53cee97 100644 --- a/docker/README.md +++ b/docker/README.md @@ -100,12 +100,18 @@ Below we list all settings from `mempool-config.json` and the corresponding over "BLOCK_WEIGHT_UNITS": 4000000, "INITIAL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8, + "BLOCKS_SUMMARIES_INDEXING": false, "PRICE_FEED_UPDATE_INTERVAL": 600, "USE_SECOND_NODE_FOR_MINFEE": false, "EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"], "STDOUT_LOG_MIN_PRIORITY": "info", + "INDEXING_BLOCKS_AMOUNT": false, + "AUTOMATIC_BLOCK_REINDEXING": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json", - "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master" + "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "ADVANCED_GBT_AUDIT": false, + "ADVANCED_GBT_MEMPOOL": false, + "CPFP_INDEXING": false, }, ``` @@ -125,15 +131,25 @@ Corresponding `docker-compose.yml` overrides: MEMPOOL_BLOCK_WEIGHT_UNITS: "" MEMPOOL_INITIAL_BLOCKS_AMOUNT: "" MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: "" + MEMPOOL_BLOCKS_SUMMARIES_INDEXING: "" MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: "" MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: "" MEMPOOL_EXTERNAL_ASSETS: "" MEMPOOL_STDOUT_LOG_MIN_PRIORITY: "" + MEMPOOL_INDEXING_BLOCKS_AMOUNT: "" + MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: "" MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: "" + MEMPOOL_ADVANCED_GBT_AUDIT: "" + MEMPOOL_ADVANCED_GBT_MEMPOOL: "" + MEMPOOL_CPFP_INDEXING: "" ... ``` +`ADVANCED_GBT_AUDIT` AND `ADVANCED_GBT_MEMPOOL` enable a more accurate (but slower) block prediction algorithm for the block audit feature and the projected mempool-blocks respectively. + +`CPFP_INDEXING` enables indexing CPFP (Child Pays For Parent) information for the last `INDEXING_BLOCKS_AMOUNT` blocks. +
`mempool-config.json`: diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 378bba8db..2e3826f1d 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -22,7 +22,10 @@ "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__, - "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__ + "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__, + "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, + "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, + "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__ }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index c8e2a1502..b5b6e863d 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -27,6 +27,9 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false} __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} +__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false} +__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false} +__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} # CORE_RPC __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} @@ -136,6 +139,8 @@ sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT_ sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json +sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json +sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index 6a263de99..3e2210360 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -31,6 +31,9 @@ __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} __BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets} __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} __LIGHTNING__=${LIGHTNING:=false} +__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} +__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} +__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} # Export as environment variables to be used by envsubst export __TESTNET_ENABLED__ @@ -52,6 +55,9 @@ export __LIQUID_WEBSITE_URL__ export __BISQ_WEBSITE_URL__ export __MINING_DASHBOARD__ export __LIGHTNING__ +export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ +export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ +export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ folder=$(find /var/www/mempool -name "config.js" | xargs dirname) echo ${folder} diff --git a/frontend/README.md b/frontend/README.md index df0d8b310..4bfca4fe8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -127,7 +127,7 @@ https://www.transifex.com/mempool/mempool/dashboard/ * Thai @Gusb3ll * Turkish @stackmore * Ukrainian @volbil -* Vietnamese @bitcoin_vietnam +* Vietnamese @BitcoinvnNews * Chinese @wdljt * Russian @TonyCrusoe @Bitconan * Romanian @mirceavesa diff --git a/frontend/angular.json b/frontend/angular.json index e445f7c22..25d2a302c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -137,6 +137,10 @@ "hi": { "translation": "src/locale/messages.hi.xlf", "baseHref": "/hi/" + }, + "lt": { + "translation": "src/locale/messages.lt.xlf", + "baseHref": "/lt/" } } }, diff --git a/frontend/cypress/e2e/liquid/liquid.spec.ts b/frontend/cypress/e2e/liquid/liquid.spec.ts index 1e7c4649d..e24b19fad 100644 --- a/frontend/cypress/e2e/liquid/liquid.spec.ts +++ b/frontend/cypress/e2e/liquid/liquid.spec.ts @@ -7,7 +7,6 @@ describe('Liquid', () => { cy.intercept('/liquid/api/blocks/').as('blocks'); cy.intercept('/liquid/api/tx/**/outspends').as('outspends'); cy.intercept('/liquid/api/block/**/txs/**').as('block-txs'); - cy.intercept('/resources/pools.json').as('pools'); Cypress.Commands.add('waitForBlockData', () => { cy.wait('@socket'); diff --git a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts index 3ca23425e..5cf6cf331 100644 --- a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts +++ b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts @@ -7,7 +7,6 @@ describe('Liquid Testnet', () => { cy.intercept('/liquidtestnet/api/blocks/').as('blocks'); cy.intercept('/liquidtestnet/api/tx/**/outspends').as('outspends'); cy.intercept('/liquidtestnet/api/block/**/txs/**').as('block-txs'); - cy.intercept('/resources/pools.json').as('pools'); Cypress.Commands.add('waitForBlockData', () => { cy.wait('@socket'); diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index 4cc9a64c9..d6fe94dac 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -41,7 +41,6 @@ describe('Mainnet', () => { // cy.intercept('/api/v1/block/*/summary').as('block-summary'); // cy.intercept('/api/v1/outspends/*').as('outspends'); // cy.intercept('/api/tx/*/outspends').as('tx-outspends'); - // cy.intercept('/resources/pools.json').as('pools'); // Search Auto Complete cy.intercept('/api/address-prefix/1wiz').as('search-1wiz'); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2efccf07d..bab0817b2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -57,8 +57,8 @@ "typescript": "~4.6.4" }, "optionalDependencies": { - "@cypress/schematic": "~2.3.0", - "cypress": "^11.2.0", + "@cypress/schematic": "^2.4.0", + "cypress": "^12.1.0", "cypress-fail-on-console-error": "~4.0.2", "cypress-wait-until": "^1.7.2", "mock-socket": "~9.1.5", @@ -3225,9 +3225,9 @@ } }, "node_modules/@cypress/schematic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.3.0.tgz", - "integrity": "sha512-LBKX20MUUYF2Xu+1+KpVbLCoMvt2Osa80yQfonduVsLJ/p8JxtLHqufuf/ryJp9Gm9R5sDfk/YhHL+rB7a+gsg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.4.0.tgz", + "integrity": "sha512-aor8hQ+gMXqx/ASdo7CUGo/sMEWwwfSRsLr99rM2GjvW+pZnCKKTnRG4UPf8Ro9SevLJj7KRZAZWxa5MAkJzZA==", "optional": true, "dependencies": { "@angular-devkit/architect": "^0.1402.1", @@ -7019,9 +7019,9 @@ "peer": true }, "node_modules/cypress": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz", - "integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz", + "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -7072,7 +7072,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=12.0.0" + "node": "^14.0.0 || ^16.0.0 || >=18.0.0" } }, "node_modules/cypress-fail-on-console-error": { @@ -19345,9 +19345,9 @@ } }, "@cypress/schematic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.3.0.tgz", - "integrity": "sha512-LBKX20MUUYF2Xu+1+KpVbLCoMvt2Osa80yQfonduVsLJ/p8JxtLHqufuf/ryJp9Gm9R5sDfk/YhHL+rB7a+gsg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.4.0.tgz", + "integrity": "sha512-aor8hQ+gMXqx/ASdo7CUGo/sMEWwwfSRsLr99rM2GjvW+pZnCKKTnRG4UPf8Ro9SevLJj7KRZAZWxa5MAkJzZA==", "optional": true, "requires": { "@angular-devkit/architect": "^0.1402.1", @@ -22282,9 +22282,9 @@ "peer": true }, "cypress": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz", - "integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz", + "integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==", "optional": true, "requires": { "@cypress/request": "^2.88.10", diff --git a/frontend/package.json b/frontend/package.json index 325bb6bc3..174392ef0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -109,8 +109,8 @@ "typescript": "~4.6.4" }, "optionalDependencies": { - "@cypress/schematic": "~2.3.0", - "cypress": "^11.2.0", + "@cypress/schematic": "^2.4.0", + "cypress": "^12.1.0", "cypress-fail-on-console-error": "~4.0.2", "cypress-wait-until": "^1.7.2", "mock-socket": "~9.1.5", diff --git a/frontend/proxy.conf.js b/frontend/proxy.conf.js index ab2240c03..f5384bef0 100644 --- a/frontend/proxy.conf.js +++ b/frontend/proxy.conf.js @@ -76,7 +76,7 @@ PROXY_CONFIG = [ if (configContent && configContent.BASE_MODULE == "liquid") { PROXY_CONFIG.push({ - context: ['/resources/pools.json', + context: [ '/resources/assets.json', '/resources/assets.minimal.json', '/resources/assets-testnet.json', '/resources/assets-testnet.minimal.json'], target: "https://liquid.network", @@ -85,7 +85,7 @@ if (configContent && configContent.BASE_MODULE == "liquid") { }); } else { PROXY_CONFIG.push({ - context: ['/resources/pools.json', '/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'], + context: ['/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'], target: "https://mempool.space", secure: false, changeOrigin: true, diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index 9cd374cd0..95e1e756e 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -120,7 +120,7 @@ export const languages: Language[] = [ { code: 'he', name: 'עברית' }, // Hebrew { code: 'ka', name: 'ქართული' }, // Georgian // { code: 'lv', name: 'Latviešu' }, // Latvian -// { code: 'lt', name: 'Lietuvių' }, // Lithuanian + { code: 'lt', name: 'Lietuvių' }, // Lithuanian { code: 'hu', name: 'Magyar' }, // Hungarian { code: 'mk', name: 'Македонски' }, // Macedonian // { code: 'ms', name: 'Bahasa Melayu' }, // Malay diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6ed7c43f9..b7bd1526f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './components/app/app.component'; import { ElectrsApiService } from './services/electrs-api.service'; import { StateService } from './services/state.service'; +import { CacheService } from './services/cache.service'; import { EnterpriseService } from './services/enterprise.service'; import { WebsocketService } from './services/websocket.service'; import { AudioService } from './services/audio.service'; @@ -23,6 +24,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'; const providers = [ ElectrsApiService, StateService, + CacheService, WebsocketService, AudioService, SeoService, diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index be5bdeead..f4b3d0ca5 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -36,7 +36,9 @@ export class AddressLabelsComponent implements OnChanges { handleChannel() { const type = this.vout ? 'open' : 'close'; - this.label = `Channel ${type}: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`; + const leftNodeName = this.channel.node_left.alias || this.channel.node_left.public_key.substring(0, 10); + const rightNodeName = this.channel.node_right.alias || this.channel.node_right.public_key.substring(0, 10); + this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`; } handleVin() { diff --git a/frontend/src/app/components/app/app.component.ts b/frontend/src/app/components/app/app.component.ts index d9d6f77d6..c7ca798ae 100644 --- a/frontend/src/app/components/app/app.component.ts +++ b/frontend/src/app/components/app/app.component.ts @@ -42,6 +42,10 @@ export class AppComponent implements OnInit { if (event.target instanceof HTMLInputElement) { return; } + // prevent arrow key horizontal scrolling + if(["ArrowLeft","ArrowRight"].indexOf(event.code) > -1) { + event.preventDefault(); + } this.stateService.keyNavigation$.next(event); } 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 6da576e99..37225ea1d 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 @@ -77,6 +77,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On cancelAnimationFrame(this.animationFrameRequest); clearTimeout(this.animationHeartBeat); } + this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost); + this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored); } clear(direction): void { diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html index 2535486de..7300182f3 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -53,13 +53,13 @@ Miner + [class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> {{ block?.extras.pool.name }} + [class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'"> {{ block?.extras.pool.name }} diff --git a/frontend/src/app/components/block/block-preview.component.scss b/frontend/src/app/components/block/block-preview.component.scss index 45e9f9292..829050c4c 100644 --- a/frontend/src/app/components/block/block-preview.component.scss +++ b/frontend/src/app/components/block/block-preview.component.scss @@ -56,3 +56,7 @@ ::ng-deep .symbol { font-size: 24px; } + +.badge { + transition: none; +} diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 2c240c250..08ea04ca9 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -32,10 +32,10 @@
- -
- - +
+
+ + @@ -54,83 +54,28 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hash {{ block.id | shortenString : 13 }} Weight
Block health - {{ blockAudit.matchRate }}% - Unknown + {{ blockAudit?.matchRate }}% + + + Unknown + + + + +
Fee span{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} sat/vB
Median fee~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB
Total fees - - - - - -   -
Subsidy + fees: - - - - -
Total fees
Subsidy + fees:
Miner - - {{ block.extras.pool.name }} - - - - {{ block.extras.pool.name }} - -
-
-
- -
- - + + @@ -143,114 +88,18 @@ - + - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
Fee span{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} sat/vB
Median fee~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB
Total fees - - - - - -   -
Subsidy + fees: - - - - -
Total fees
Subsidy + fees:
Miner - - {{ block.extras.pool.name }} - - - - {{ block.extras.pool.name }} - -
- + +
+
- - - - - - - - - - - - - - - - +
@@ -263,11 +112,93 @@ [flip]="false" (txClickEvent)="onTxClick($event)" > +
+ + + + Fee span + {{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} sat/vB + + + Median fee + ~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB + + + + Total fees + + + + + + + + +   + + + + + Subsidy + fees: + + + + + + + + + + + Total fees + + + + Subsidy + fees: + + + + + Miner + + + {{ block.extras.pool.name }} + + + + + {{ block.extras.pool.name }} + + + + + + + + + + + + + + + + + + + + + + +
@@ -283,15 +214,21 @@

Projected Block

- +
+ + +

Actual Block

- +
+ + +
@@ -413,5 +350,17 @@ + + + + Why is this block empty? + + +

diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index 69002de79..931912e4e 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -202,4 +202,24 @@ h1 { &.active, &:hover { border-color: white; } +} + +.block-graph-wrapper { + position: relative; +} + +.info-bubble-link { + position: absolute; + display: block; + top: 2em; + left: 50%; + margin: auto; + text-align: center; + padding: 0.5em 1em; + font-size: 80%; + transform: translateX(-50%); + + .ng-fa-icon { + margin-right: 1em; + } } \ No newline at end of file diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index e4806f8c6..f04b4ec9c 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -43,6 +43,7 @@ export class BlockComponent implements OnInit, OnDestroy { strippedTransactions: TransactionStripped[]; overviewTransitionDirection: string; isLoadingOverview = true; + isLoadingAudit = true; error: any; blockSubsidy: number; fees: number; @@ -137,7 +138,6 @@ export class BlockComponent implements OnInit, OnDestroy { this.page = 1; this.error = undefined; this.fees = undefined; - this.stateService.markBlock$.next({}); this.auditDataMissing = false; if (history.state.data && history.state.data.blockHeight) { @@ -297,13 +297,18 @@ export class BlockComponent implements OnInit, OnDestroy { this.auditSubscription = block$.pipe( startWith(null), pairwise(), - switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id) - .pipe( - catchError((err) => { - this.overviewError = err; - return of([]); - }) - ) + switchMap(([prevBlock, block]) => { + this.isLoadingAudit = true; + this.blockAudit = null; + return this.apiService.getBlockAudit$(block.id) + .pipe( + catchError((err) => { + this.overviewError = err; + this.isLoadingAudit = false; + return of([]); + }) + ); + } ), filter((response) => response != null), map((response) => { @@ -375,12 +380,14 @@ export class BlockComponent implements OnInit, OnDestroy { console.log(err); this.error = err; this.isLoadingOverview = false; + this.isLoadingAudit = false; return of(null); }), ).subscribe((blockAudit) => { this.blockAudit = blockAudit; this.setupBlockGraphs(); this.isLoadingOverview = false; + this.isLoadingAudit = false; }); } 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 6bd617435..29df378a4 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -1,36 +1,55 @@ -
-
-
-   -
- {{ block.height }} +
+
+ +
+   + +
+
+ ~{{ block?.extras?.medianFee | number:feeRounding }} sat/vB +
+
+ {{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} sat/vB +
+
+   +
+
+ +
+
+
+ + {{ i }} transaction + {{ i }} transactions +
+
+
+
-
-
- ~{{ block?.extras?.medianFee | number:feeRounding }} sat/vB + + + +
+
-
- {{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} sat/vB + + + + +
+
-
- -
-
-
- - {{ i }} transaction - {{ i }} transactions -
-
-
- -
+ +
-
+
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 adde4a945..64bfd2379 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -25,6 +25,10 @@ transition: background 2s, left 2s, transform 1s; } +.mined-block.placeholder-block { + background: none !important; +} + .block-size { font-size: 16px; font-weight: bold; @@ -96,6 +100,16 @@ transform-origin: top; } +.bitcoin-block.placeholder-block::after { + content: none; + background: 0; +} + +.bitcoin-block.placeholder-block::before { + content: none; + background: 0; +} + .black-background { background-color: #11131f; z-index: 100; 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 8ac925eaf..fd8819a6f 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,10 +1,16 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; 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'; +import { CacheService } from 'src/app/services/cache.service'; + +interface BlockchainBlock extends BlockExtended { + placeholder?: boolean; + loading?: boolean; +} @Component({ selector: 'app-blockchain-blocks', @@ -12,13 +18,19 @@ import { config } from 'process'; styleUrls: ['./blockchain-blocks.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class BlockchainBlocksComponent implements OnInit, OnDestroy { +export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { + @Input() static: boolean = false; + @Input() offset: number = 0; + @Input() height: number = 0; + @Input() count: number = 8; + specialBlocks = specialBlocks; network = ''; - blocks: BlockExtended[] = []; + blocks: BlockchainBlock[] = []; emptyBlocks: BlockExtended[] = this.mountEmptyBlocks(); markHeight: number; blocksSubscription: Subscription; + blockPageSubscription: Subscription; networkSubscription: Subscription; tabHiddenSubscription: Subscription; markBlockSubscription: Subscription; @@ -31,7 +43,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { arrowVisible = false; arrowLeftPx = 30; blocksFilled = false; - transition = '1s'; + arrowTransition = '1s'; showMiningInfo = false; timeLtrSubscription: Subscription; timeLtr: boolean; @@ -47,6 +59,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { constructor( public stateService: StateService, + public cacheService: CacheService, private cd: ChangeDetectorRef, private location: Location, ) { @@ -75,44 +88,52 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network); this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); - this.blocksSubscription = this.stateService.blocks$ - .subscribe(([block, txConfirmed]) => { - if (this.blocks.some((b) => b.height === block.height)) { - return; - } + if (!this.static) { + this.blocksSubscription = this.stateService.blocks$ + .subscribe(([block, txConfirmed]) => { + if (this.blocks.some((b) => b.height === block.height)) { + return; + } - if (this.blocks.length && block.height !== this.blocks[0].height + 1) { - this.blocks = []; - this.blocksFilled = false; - } + if (this.blocks.length && block.height !== this.blocks[0].height + 1) { + this.blocks = []; + this.blocksFilled = false; + } - this.blocks.unshift(block); - this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); + this.blocks.unshift(block); + this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT); - if (this.blocksFilled && !this.tabHidden && block.extras) { - block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2; - } + if (txConfirmed) { + this.markHeight = block.height; + this.moveArrowToPosition(true, true); + } else { + this.moveArrowToPosition(true, false); + } - if (txConfirmed) { - this.markHeight = block.height; - this.moveArrowToPosition(true, true); - } else { - this.moveArrowToPosition(true, false); - } - - this.blockStyles = []; - this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); - setTimeout(() => { this.blockStyles = []; - this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b))); - this.cd.markForCheck(); - }, 50); + if (this.blocksFilled) { + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205))); + setTimeout(() => { + this.blockStyles = []; + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); + this.cd.markForCheck(); + }, 50); + } else { + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); + } - if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { - this.blocksFilled = true; + if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) { + this.blocksFilled = true; + } + this.cd.markForCheck(); + }); + } else { + this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => { + if (block.height <= this.height && block.height > this.height - this.count) { + this.onBlockLoaded(block); } - this.cd.markForCheck(); }); + } this.markBlockSubscription = this.stateService.markBlock$ .subscribe((state) => { @@ -123,10 +144,26 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { this.moveArrowToPosition(false); this.cd.markForCheck(); }); + + if (this.static) { + this.updateStaticBlocks(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (this.static) { + const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1); + this.updateStaticBlocks(animateSlide); + } } ngOnDestroy() { - this.blocksSubscription.unsubscribe(); + if (this.blocksSubscription) { + this.blocksSubscription.unsubscribe(); + } + if (this.blockPageSubscription) { + this.blockPageSubscription.unsubscribe(); + } this.networkSubscription.unsubscribe(); this.tabHiddenSubscription.unsubscribe(); this.markBlockSubscription.unsubscribe(); @@ -142,13 +179,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight); if (blockindex > -1) { if (!animate) { - this.transition = 'inherit'; + this.arrowTransition = 'inherit'; } this.arrowVisible = true; if (newBlockFromLeft) { this.arrowLeftPx = blockindex * 155 + 30 - 205; setTimeout(() => { - this.transition = '2s'; + this.arrowTransition = '2s'; this.arrowLeftPx = blockindex * 155 + 30; this.cd.markForCheck(); }, 50); @@ -156,45 +193,117 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { this.arrowLeftPx = blockindex * 155 + 30; if (!animate) { setTimeout(() => { - this.transition = '2s'; + this.arrowTransition = '2s'; this.cd.markForCheck(); - }); + }, 50); } } + } else { + this.arrowVisible = false; } } - trackByBlocksFn(index: number, item: BlockExtended) { + trackByBlocksFn(index: number, item: BlockchainBlock) { return item.height; } - getStyleForBlock(block: BlockExtended) { + updateStaticBlocks(animateSlide: boolean = false) { + // reset blocks + this.blocks = []; + this.blockStyles = []; + while (this.blocks.length < this.count) { + const height = this.height - this.blocks.length; + let block; + if (height >= 0) { + this.cacheService.loadBlock(height); + block = this.cacheService.getCachedBlock(height) || null; + } + this.blocks.push(block || { + placeholder: height < 0, + loading: height >= 0, + id: '', + height, + version: 0, + timestamp: 0, + bits: 0, + nonce: 0, + difficulty: 0, + merkle_root: '', + tx_count: 0, + size: 0, + weight: 0, + previousblockhash: '', + }); + } + this.blocks = this.blocks.slice(0, this.count); + this.blockStyles = []; + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0))); + this.cd.markForCheck(); + if (animateSlide) { + // animate blocks slide right + setTimeout(() => { + this.blockStyles = []; + this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); + this.cd.markForCheck(); + }, 50); + this.moveArrowToPosition(true, true); + } else { + this.moveArrowToPosition(false, false); + } + } + + onBlockLoaded(block: BlockExtended) { + const blockIndex = this.height - block.height; + if (blockIndex >= 0 && blockIndex < this.blocks.length) { + this.blocks[blockIndex] = block; + this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex); + } + this.cd.markForCheck(); + } + + getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) { + if (!block || block.placeholder) { + return this.getStyleForPlaceholderBlock(index, animateEnterFrom); + } else if (block.loading) { + return this.getStyleForLoadingBlock(index, animateEnterFrom); + } const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100; let addLeft = 0; - if (block?.extras?.stage === 1) { - block.extras.stage = 2; - addLeft = -205; + if (animateEnterFrom) { + addLeft = animateEnterFrom || 0; } return { - left: addLeft + 155 * this.blocks.indexOf(block) + 'px', + left: addLeft + 155 * index + 'px', background: `repeating-linear-gradient( #2d3348, #2d3348 ${greenBackgroundHeight}%, ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, ${this.gradientColors[this.network][1]} 100% )`, + transition: animateEnterFrom ? 'background 2s, transform 1s' : null, }; } - getStyleForEmptyBlock(block: BlockExtended) { - let addLeft = 0; + getStyleForLoadingBlock(index: number, animateEnterFrom: number = 0) { + const addLeft = animateEnterFrom || 0; - if (block?.extras?.stage === 1) { - block.extras.stage = 2; - addLeft = -205; - } + return { + left: addLeft + (155 * index) + 'px', + background: "#2d3348", + }; + } + + getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) { + const addLeft = animateEnterFrom || 0; + return { + left: addLeft + (155 * index) + 'px', + }; + } + + getStyleForEmptyBlock(block: BlockExtended, animateEnterFrom: number = 0) { + const addLeft = animateEnterFrom || 0; return { left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px', @@ -219,7 +328,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { weight: 0, previousblockhash: '', matchRate: 0, - stage: 0, }); } return emptyBlocks; diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index 66ae8dd43..ad2e5e86a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -2,10 +2,14 @@
- - +
+ + + + +
-
+
diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index df609ff40..63ca22626 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -72,6 +72,15 @@ position: relative; } +.scroll-spacer { + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 1px; + pointer-events: none; +} + .loading-block { position: absolute; text-align: center; diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index e99b3532d..0ad3625ea 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Subscription } from 'rxjs'; import { StateService } from '../../services/state.service'; @@ -9,6 +9,11 @@ import { StateService } from '../../services/state.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class BlockchainComponent implements OnInit, OnDestroy { + @Input() pages: any[] = []; + @Input() pageIndex: number; + @Input() blocksPerPage: number = 8; + @Input() minScrollWidth: number = 0; + network: string; timeLtrSubscription: Subscription; timeLtr: boolean = this.stateService.timeLtr.value; @@ -29,6 +34,10 @@ export class BlockchainComponent implements OnInit, OnDestroy { this.timeLtrSubscription.unsubscribe(); } + trackByPageFn(index: number, item: { index: number }) { + return item.index; + } + 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 628efb51b..0dd24d116 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -14,7 +14,7 @@ i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool Timestamp Mined - Health Reward @@ -27,7 +27,7 @@ - {{ block.height }} + {{ block.height }}
@@ -46,16 +46,23 @@ - -
-
-
- {{ auditScores[block.id] }}% - ~ -
-
-
+ {{ auditScores[block.id] }}% + + + Unknown + + + + + 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 700032225..93f7814cf 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -23,6 +23,7 @@ export class BlocksList implements OnInit, OnDestroy { indexingAvailable = false; isLoading = true; + loadingScores = true; fromBlockHeight = undefined; paginationMaxSize: number; page = 1; @@ -113,6 +114,7 @@ export class BlocksList implements OnInit, OnDestroy { if (this.indexingAvailable) { this.auditScoreSubscription = this.fromHeightSubject.pipe( switchMap((fromBlockHeight) => { + this.loadingScores = true; return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight) .pipe( catchError(() => { @@ -124,6 +126,7 @@ export class BlocksList implements OnInit, OnDestroy { Object.values(scores).forEach(score => { this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null; }); + this.loadingScores = false; }); this.latestScoreSubscription = this.stateService.blocks$.pipe( 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 4e38ea6e0..b881c6ea7 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -5,7 +5,7 @@
-
diff --git a/frontend/src/app/components/search-form/search-form.component.scss b/frontend/src/app/components/search-form/search-form.component.scss index 7c8161196..d59acadb9 100644 --- a/frontend/src/app/components/search-form/search-form.component.scss +++ b/frontend/src/app/components/search-form/search-form.component.scss @@ -43,9 +43,6 @@ form { @media (min-width: 1200px) { min-width: 300px; } - input { - border: 0px; - } .btn { width: 100px; } diff --git a/frontend/src/app/components/start/start.component.html b/frontend/src/app/components/start/start.component.html index 89b6efdc3..c3277cb9a 100644 --- a/frontend/src/app/components/start/start.component.html +++ b/frontend/src/app/components/start/start.component.html @@ -11,8 +11,9 @@
- +
diff --git a/frontend/src/app/components/start/start.component.ts b/frontend/src/app/components/start/start.component.ts index 37c94baa3..558e6f909 100644 --- a/frontend/src/app/components/start/start.component.ts +++ b/frontend/src/app/components/start/start.component.ts @@ -19,16 +19,51 @@ export class StartComponent implements OnInit, OnDestroy { blockchainScrollLeftInit: number; timeLtrSubscription: Subscription; timeLtr: boolean = this.stateService.timeLtr.value; + chainTipSubscription: Subscription; + chainTip: number = -1; + markBlockSubscription: Subscription; @ViewChild('blockchainContainer') blockchainContainer: ElementRef; + isMobile: boolean = false; + blockWidth = 155; + blocksPerPage: number = 1; + pageWidth: number; + firstPageWidth: number; + minScrollWidth: number; + pageIndex: number = 0; + pages: any[] = []; + pendingMark: number | void = null; + constructor( private stateService: StateService, ) { } ngOnInit() { + this.firstPageWidth = 40 + (this.blockWidth * this.stateService.env.KEEP_BLOCKS_AMOUNT); + this.onResize(); + this.updatePages(); this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => { this.timeLtr = !!ltr; }); + this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => { + this.chainTip = height; + this.updatePages(); + if (this.pendingMark != null) { + this.scrollToBlock(this.pendingMark); + this.pendingMark = null; + } + }); + this.markBlockSubscription = this.stateService.markBlock$.subscribe((mark) => { + if (mark?.blockHeight != null) { + if (this.chainTip >=0) { + if (!this.blockInViewport(mark.blockHeight)) { + this.scrollToBlock(mark.blockHeight); + } + } else { + this.pendingMark = mark.blockHeight; + } + } + }); this.stateService.blocks$ .subscribe((blocks: any) => { if (this.stateService.network !== '') { @@ -55,6 +90,34 @@ export class StartComponent implements OnInit, OnDestroy { }); } + @HostListener('window:resize', ['$event']) + onResize(): void { + this.isMobile = window.innerWidth <= 767.98; + let firstVisibleBlock; + let offset; + if (this.blockchainContainer?.nativeElement != null) { + this.pages.forEach(page => { + const left = page.offset - this.getConvertedScrollOffset(); + const right = left + this.pageWidth; + if (left <= 0 && right > 0) { + const blockIndex = Math.max(0, Math.floor(left / -this.blockWidth)); + firstVisibleBlock = page.height - blockIndex; + offset = left + (blockIndex * this.blockWidth); + } + }); + } + + this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth); + this.pageWidth = this.blocksPerPage * this.blockWidth; + this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2); + + if (firstVisibleBlock != null) { + this.scrollToBlock(firstVisibleBlock, offset); + } else { + this.updatePages(); + } + } + onMouseDown(event: MouseEvent) { this.mouseDragStartX = event.clientX; this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft; @@ -70,7 +133,7 @@ export class StartComponent implements OnInit, OnDestroy { if (this.mouseDragStartX != null) { this.stateService.setBlockScrollingInProgress(true); this.blockchainContainer.nativeElement.scrollLeft = - this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX + this.blockchainScrollLeftInit + this.mouseDragStartX - event.clientX; } } @HostListener('document:mouseup', []) @@ -79,7 +142,149 @@ export class StartComponent implements OnInit, OnDestroy { this.stateService.setBlockScrollingInProgress(false); } + onScroll(e) { + const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1]; + // compensate for css transform + const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); + const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation; + const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation; + const scrollLeft = this.getConvertedScrollOffset(); + if (scrollLeft > backThreshold) { + if (this.shiftPagesBack()) { + this.addConvertedScrollOffset(-this.pageWidth); + this.blockchainScrollLeftInit -= this.pageWidth; + } + } else if (scrollLeft < forwardThreshold) { + if (this.shiftPagesForward()) { + this.addConvertedScrollOffset(this.pageWidth); + this.blockchainScrollLeftInit += this.pageWidth; + } + } + } + + scrollToBlock(height, blockOffset = 0) { + if (!this.blockchainContainer?.nativeElement) { + setTimeout(() => { this.scrollToBlock(height, blockOffset); }, 50); + return; + } + const targetHeight = this.isMobile ? height - 1 : height; + const viewingPageIndex = this.getPageIndexOf(targetHeight); + const pages = []; + this.pageIndex = Math.max(viewingPageIndex - 1, 0); + let viewingPage = this.getPageAt(viewingPageIndex); + const isLastPage = viewingPage.height < this.blocksPerPage; + if (isLastPage) { + this.pageIndex = Math.max(viewingPageIndex - 2, 0); + viewingPage = this.getPageAt(viewingPageIndex); + } + const left = viewingPage.offset - this.getConvertedScrollOffset(); + const blockIndex = viewingPage.height - targetHeight; + const targetOffset = (this.blockWidth * blockIndex) + left; + let deltaOffset = targetOffset - blockOffset; + + if (isLastPage) { + pages.push(this.getPageAt(viewingPageIndex - 2)); + } + if (viewingPageIndex > 1) { + pages.push(this.getPageAt(viewingPageIndex - 1)); + } + if (viewingPageIndex > 0) { + pages.push(viewingPage); + } + if (!isLastPage) { + pages.push(this.getPageAt(viewingPageIndex + 1)); + } + if (viewingPageIndex === 0) { + pages.push(this.getPageAt(viewingPageIndex + 2)); + } + + this.pages = pages; + this.addConvertedScrollOffset(deltaOffset); + } + + updatePages() { + const pages = []; + if (this.pageIndex > 0) { + pages.push(this.getPageAt(this.pageIndex)); + } + pages.push(this.getPageAt(this.pageIndex + 1)); + pages.push(this.getPageAt(this.pageIndex + 2)); + this.pages = pages; + } + + shiftPagesBack(): boolean { + const nextPage = this.getPageAt(this.pageIndex + 3); + if (nextPage.height >= 0) { + this.pageIndex++; + this.pages.forEach(page => page.offset -= this.pageWidth); + if (this.pageIndex !== 1) { + this.pages.shift(); + } + this.pages.push(this.getPageAt(this.pageIndex + 2)); + return true; + } else { + return false; + } + } + + shiftPagesForward(): boolean { + if (this.pageIndex > 0) { + this.pageIndex--; + this.pages.forEach(page => page.offset += this.pageWidth); + this.pages.pop(); + if (this.pageIndex) { + this.pages.unshift(this.getPageAt(this.pageIndex)); + } + return true; + } + return false; + } + + getPageAt(index: number) { + const height = this.chainTip - 8 - ((index - 1) * this.blocksPerPage) + return { + offset: this.firstPageWidth + (this.pageWidth * (index - 1 - this.pageIndex)), + height: height, + depth: this.chainTip - height, + index: index, + }; + } + + getPageIndexOf(height: number): number { + const delta = this.chainTip - 8 - height; + return Math.max(0, Math.floor(delta / this.blocksPerPage) + 1); + } + + blockInViewport(height: number): boolean { + const firstHeight = this.pages[0].height; + const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5); + const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation; + const xPos = firstX + ((firstHeight - height) * 155); + return xPos > -55 && xPos < (window.innerWidth - 100); + } + + getConvertedScrollOffset(): number { + if (this.timeLtr) { + return -this.blockchainContainer?.nativeElement?.scrollLeft || 0; + } else { + return this.blockchainContainer?.nativeElement?.scrollLeft || 0; + } + } + + addConvertedScrollOffset(offset: number): void { + if (!this.blockchainContainer?.nativeElement) { + return; + } + if (this.timeLtr) { + this.blockchainContainer.nativeElement.scrollLeft -= offset; + } else { + this.blockchainContainer.nativeElement.scrollLeft += offset; + } + } + ngOnDestroy() { this.timeLtrSubscription.unsubscribe(); + this.chainTipSubscription.unsubscribe(); + this.markBlockSubscription.unsubscribe(); } } diff --git a/frontend/src/app/components/transaction/transaction-preview.component.ts b/frontend/src/app/components/transaction/transaction-preview.component.ts index 9d2d502b4..6db0e588c 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.ts +++ b/frontend/src/app/components/transaction/transaction-preview.component.ts @@ -11,6 +11,7 @@ import { import { Transaction, Vout } from '../../interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, from } from 'rxjs'; import { StateService } from '../../services/state.service'; +import { CacheService } from '../../services/cache.service'; import { OpenGraphService } from '../../services/opengraph.service'; import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; @@ -45,6 +46,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private electrsApiService: ElectrsApiService, private stateService: StateService, + private cacheService: CacheService, private apiService: ApiService, private seoService: SeoService, private openGraphService: OpenGraphService, @@ -97,7 +99,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy { }), switchMap(() => { let transactionObservable$: Observable; - const cached = this.stateService.getTxFromCache(this.txId); + const cached = this.cacheService.getTxFromCache(this.txId); if (cached && cached.fee !== -1) { transactionObservable$ = of(cached); } else { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 575c00637..cd85d0f4f 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -13,6 +13,7 @@ import { import { Transaction } from '../../interfaces/electrs.interface'; import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from, throwError } from 'rxjs'; import { StateService } from '../../services/state.service'; +import { CacheService } from '../../services/cache.service'; import { WebsocketService } from '../../services/websocket.service'; import { AudioService } from '../../services/audio.service'; import { ApiService } from '../../services/api.service'; @@ -74,6 +75,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { private relativeUrlPipe: RelativeUrlPipe, private electrsApiService: ElectrsApiService, private stateService: StateService, + private cacheService: CacheService, private websocketService: WebsocketService, private audioService: AudioService, private apiService: ApiService, @@ -131,26 +133,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.cpfpInfo = null; return; } - if (cpfpInfo.effectiveFeePerVsize) { - this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; - } else { - const lowerFeeParents = cpfpInfo.ancestors.filter( - (parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize - ); - let totalWeight = - this.tx.weight + - lowerFeeParents.reduce((prev, val) => prev + val.weight, 0); - let totalFees = - this.tx.fee + - lowerFeeParents.reduce((prev, val) => prev + val.fee, 0); - - if (cpfpInfo?.bestDescendant) { - totalWeight += cpfpInfo?.bestDescendant.weight; - totalFees += cpfpInfo?.bestDescendant.fee; - } - - this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); + // merge ancestors/descendants + const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])]; + if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) { + relatives.push(cpfpInfo.bestDescendant); } + let totalWeight = + this.tx.weight + + relatives.reduce((prev, val) => prev + val.weight, 0); + let totalFees = + this.tx.fee + + relatives.reduce((prev, val) => prev + val.fee, 0); + + this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4); + if (!this.tx.status.confirmed) { this.stateService.markBlock$.next({ txFeePerVSize: this.tx.effectiveFeePerVsize, @@ -203,7 +199,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }), switchMap(() => { let transactionObservable$: Observable; - const cached = this.stateService.getTxFromCache(this.txId); + const cached = this.cacheService.getTxFromCache(this.txId); if (cached && cached.fee !== -1) { transactionObservable$ = of(cached); } else { @@ -302,7 +298,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.waitingForTransaction = false; } this.rbfTransaction = rbfTransaction; - this.stateService.setTxCache([this.rbfTransaction]); + this.cacheService.setTxCache([this.rbfTransaction]); }); this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 072d71311..67df2daa2 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { StateService } from '../../services/state.service'; +import { CacheService } from '../../services/cache.service'; import { Observable, ReplaySubject, BehaviorSubject, merge, Subscription } from 'rxjs'; import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; import { ElectrsApiService } from '../../services/electrs-api.service'; @@ -44,6 +45,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { constructor( public stateService: StateService, + private cacheService: CacheService, private electrsApiService: ElectrsApiService, private apiService: ApiService, private assetsService: AssetsService, @@ -91,6 +93,9 @@ export class TransactionsListComponent implements OnInit, OnChanges { filter(() => this.stateService.env.LIGHTNING), switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)), tap((channels) => { + if (!this.transactions) { + return; + } const transactions = this.transactions.filter((tx) => !tx._channels); channels.forEach((channel, i) => { transactions[i]._channels = channel; @@ -120,7 +125,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { } this.transactionsLength = this.transactions.length; - this.stateService.setTxCache(this.transactions); + this.cacheService.setTxCache(this.transactions); this.transactions.forEach((tx) => { tx['@voutLimit'] = true; diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html index f7484fb70..9360899e6 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html @@ -88,7 +88,7 @@ - + - + = new ReplaySubject(); @@ -162,7 +163,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { let truncatedInputs = this.tx.vin.map((v, i) => { return { type: 'input', - value: v?.prevout?.value, + value: v?.prevout?.value || (v?.is_coinbase && !totalValue ? 0 : undefined), txid: v.txid, vout: v.vout, address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(), @@ -198,6 +199,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`, style: `stroke-width: ${this.combinedWeight + 1}; stroke: ${this.gradient[1]}` }; + + this.hasLine = this.inputs.reduce((line, put) => line || !put.zeroValue, false) + && this.outputs.reduce((line, put) => line || !put.zeroValue, false); } calcTotalValue(tx: Transaction): number { @@ -278,6 +282,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { lineParams.forEach((line, i) => { if (xputs[i].value === 0) { line.outerY = lastOuter + (this.zeroValueThickness / 2); + if (xputs.length === 1) { + line.outerY = (this.height / 2); + } lastOuter += this.zeroValueThickness + spacing; return; } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 2e6b94988..c35eb8098 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -24,7 +24,6 @@ export interface CpfpInfo { ancestors: Ancestor[]; descendants?: Ancestor[]; bestDescendant?: BestDescendant | null; - effectiveFeePerVsize?: number; } export interface DifficultyAdjustment { @@ -122,8 +121,6 @@ export interface BlockExtension { name: string; slug: string; } - - stage?: number; // Frontend only } export interface BlockExtended extends Block { diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 46f8b12e8..abf104e2f 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -118,7 +118,7 @@ export class NodesNetworksChartComponent implements OnInit { color: 'grey', fontSize: 15 }, - text: $localize`Indexing in progess`, + text: $localize`Indexing in progress`, left: 'center', top: 'center', }; diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 4f209e7a0..916483781 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -109,7 +109,7 @@ export class LightningStatisticsChartComponent implements OnInit { color: 'grey', fontSize: 15 }, - text: $localize`Indexing in progess`, + text: $localize`Indexing in progress`, left: 'center', top: 'center' }; diff --git a/frontend/src/app/services/cache.service.ts b/frontend/src/app/services/cache.service.ts new file mode 100644 index 000000000..be37164dd --- /dev/null +++ b/frontend/src/app/services/cache.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { firstValueFrom, Subject, Subscription} from 'rxjs'; +import { Transaction } from '../interfaces/electrs.interface'; +import { BlockExtended } from '../interfaces/node-api.interface'; +import { StateService } from './state.service'; +import { ApiService } from './api.service'; + +const BLOCK_CACHE_SIZE = 500; +const KEEP_RECENT_BLOCKS = 50; + +@Injectable({ + providedIn: 'root' +}) +export class CacheService { + loadedBlocks$ = new Subject(); + tip: number = 0; + + txCache: { [txid: string]: Transaction } = {}; + + blockCache: { [height: number]: BlockExtended } = {}; + blockLoading: { [height: number]: boolean } = {}; + copiesInBlockQueue: { [height: number]: number } = {}; + blockPriorities: number[] = []; + + constructor( + private stateService: StateService, + private apiService: ApiService, + ) { + this.stateService.blocks$.subscribe(([block]) => { + this.addBlockToCache(block); + this.clearBlocks(); + }); + this.stateService.chainTip$.subscribe((height) => { + this.tip = height; + }); + } + + setTxCache(transactions) { + this.txCache = {}; + transactions.forEach(tx => { + this.txCache[tx.txid] = tx; + }); + } + + getTxFromCache(txid) { + if (this.txCache && this.txCache[txid]) { + return this.txCache[txid]; + } else { + return null; + } + } + + addBlockToCache(block: BlockExtended) { + this.blockCache[block.height] = block; + this.bumpBlockPriority(block.height); + } + + async loadBlock(height) { + if (!this.blockCache[height] && !this.blockLoading[height]) { + const chunkSize = 10; + const maxHeight = Math.ceil(height / chunkSize) * chunkSize; + for (let i = 0; i < chunkSize; i++) { + this.blockLoading[maxHeight - i] = true; + } + const result = await firstValueFrom(this.apiService.getBlocks$(maxHeight)); + for (let i = 0; i < chunkSize; i++) { + delete this.blockLoading[maxHeight - i]; + } + if (result && result.length) { + result.forEach(block => { + this.addBlockToCache(block); + this.loadedBlocks$.next(block); + }); + } + this.clearBlocks(); + } else { + this.bumpBlockPriority(height); + } + } + + // increase the priority of a block, to delay removal + bumpBlockPriority(height) { + this.blockPriorities.push(height); + this.copiesInBlockQueue[height] = (this.copiesInBlockQueue[height] || 0) + 1; + } + + // remove lowest priority blocks from the cache + clearBlocks() { + while (Object.keys(this.blockCache).length > (BLOCK_CACHE_SIZE + KEEP_RECENT_BLOCKS) && this.blockPriorities.length > KEEP_RECENT_BLOCKS) { + const height = this.blockPriorities.shift(); + if (this.copiesInBlockQueue[height] > 1) { + this.copiesInBlockQueue[height]--; + } else if ((this.tip - height) < KEEP_RECENT_BLOCKS) { + this.bumpBlockPriority(height); + } else { + delete this.blockCache[height]; + delete this.copiesInBlockQueue[height]; + } + } + } + + getCachedBlock(height) { + return this.blockCache[height]; + } +} \ No newline at end of file diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 8a87b97e5..86efa57f8 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -104,6 +104,7 @@ export class StateService { backendInfo$ = new ReplaySubject(1); loadingIndicators$ = new ReplaySubject(1); recommendedFees$ = new ReplaySubject(1); + chainTip$ = new ReplaySubject(-1); live2Chart$ = new Subject(); @@ -111,15 +112,13 @@ export class StateService { connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); isTabHidden$: Observable; - markBlock$ = new ReplaySubject(); + markBlock$ = new BehaviorSubject({}); keyNavigation$ = new Subject(); blockScrolling$: Subject = new Subject(); timeLtr: BehaviorSubject; hideFlow: BehaviorSubject; - txCache: { [txid: string]: Transaction } = {}; - constructor( @Inject(PLATFORM_ID) private platformId: any, @Inject(LOCALE_ID) private locale: string, @@ -274,18 +273,15 @@ export class StateService { return this.network === 'liquid' || this.network === 'liquidtestnet'; } - setTxCache(transactions) { - this.txCache = {}; - transactions.forEach(tx => { - this.txCache[tx.txid] = tx; - }); + resetChainTip() { + this.latestBlockHeight = -1; + this.chainTip$.next(-1); } - - getTxFromCache(txid) { - if (this.txCache && this.txCache[txid]) { - return this.txCache[txid]; - } else { - return null; + + updateChainTip(height) { + if (height > this.latestBlockHeight) { + this.latestBlockHeight = height; + this.chainTip$.next(height); } } } diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 7cb279a08..d58ab58c9 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -70,7 +70,7 @@ export class WebsocketService { clearTimeout(this.onlineCheckTimeout); clearTimeout(this.onlineCheckTimeoutTwo); - this.stateService.latestBlockHeight = -1; + this.stateService.resetChainTip(); this.websocketSubject.complete(); this.subscription.unsubscribe(); @@ -224,12 +224,14 @@ export class WebsocketService { handleResponse(response: WebsocketResponse) { if (response.blocks && response.blocks.length) { const blocks = response.blocks; + let maxHeight = 0; blocks.forEach((block: BlockExtended) => { if (block.height > this.stateService.latestBlockHeight) { - this.stateService.latestBlockHeight = block.height; + maxHeight = Math.max(maxHeight, block.height); this.stateService.blocks$.next([block, false]); } }); + this.stateService.updateChainTip(maxHeight); } if (response.tx) { @@ -238,7 +240,7 @@ export class WebsocketService { if (response.block) { if (response.block.height > this.stateService.latestBlockHeight) { - this.stateService.latestBlockHeight = response.block.height; + this.stateService.updateChainTip(response.block.height); this.stateService.blocks$.next([response.block, !!response.txConfirmed]); } diff --git a/frontend/src/locale/messages.fr.xlf b/frontend/src/locale/messages.fr.xlf index 011ab9f43..d57c617e6 100644 --- a/frontend/src/locale/messages.fr.xlf +++ b/frontend/src/locale/messages.fr.xlf @@ -1471,6 +1471,7 @@ Community Integrations + Intégrations communautaires src/app/components/about/about.component.html 191,193 @@ -1544,6 +1545,7 @@ Multisig of + Multi-signature de src/app/components/address-labels/address-labels.component.ts 105 @@ -2027,6 +2029,7 @@ Block + Bloc src/app/components/block-audit/block-audit.component.html 7,9 @@ -2035,6 +2038,7 @@ Template vs Mined + Modèle vs Miné src/app/components/block-audit/block-audit.component.html 11,17 @@ -2121,6 +2125,7 @@ Match rate + Taux de correspondance src/app/components/block-audit/block-audit.component.html 64,67 @@ -2129,6 +2134,7 @@ Missing txs + Txs manquantes src/app/components/block-audit/block-audit.component.html 68,71 @@ -2137,6 +2143,7 @@ Added txs + Txs ajoutées src/app/components/block-audit/block-audit.component.html 72,75 @@ -2145,6 +2152,7 @@ Missing + Manquantes src/app/components/block-audit/block-audit.component.html 84,85 @@ -2153,6 +2161,7 @@ Added + Ajoutées src/app/components/block-audit/block-audit.component.html 86,92 @@ -2470,6 +2479,7 @@ No data to display yet. Try again later. + Aucune donnée à afficher pour le moment. Réessayez plus tard. src/app/components/block-prediction-graph/block-prediction-graph.component.ts 108,103 @@ -2523,6 +2533,7 @@ Block + Bloc src/app/components/block/block-preview.component.html 3,7 @@ -2535,6 +2546,7 @@ + src/app/components/block/block-preview.component.html 11,12 @@ -3250,6 +3262,7 @@ Hashrate & Difficulty + Taux de hachage & difficulté src/app/components/graphs/graphs.component.html 15,16 @@ -3258,6 +3271,7 @@ Lightning + Lightning src/app/components/graphs/graphs.component.html 31 @@ -3266,6 +3280,7 @@ Lightning Nodes Per Network + Nœuds Lightning par réseau src/app/components/graphs/graphs.component.html 34 @@ -3286,6 +3301,7 @@ Lightning Network Capacity + Capacité du réseau Lightning src/app/components/graphs/graphs.component.html 36 @@ -3306,6 +3322,7 @@ Lightning Nodes Per ISP + Nœuds Lightning par FAI src/app/components/graphs/graphs.component.html 38 @@ -3318,6 +3335,7 @@ Lightning Nodes Per Country + Nœuds Lightning par pays src/app/components/graphs/graphs.component.html 40 @@ -3334,6 +3352,7 @@ Lightning Nodes World Map + Carte du monde des nœuds Lightning src/app/components/graphs/graphs.component.html 42 @@ -3350,6 +3369,7 @@ Lightning Nodes Channels World Map + Carte du monde des canaux Lightning src/app/components/graphs/graphs.component.html 44 @@ -3470,6 +3490,7 @@ Lightning Explorer + Explorateur Lightning src/app/components/master-page/master-page.component.html 44,45 @@ -3482,6 +3503,7 @@ beta + bêta src/app/components/master-page/master-page.component.html 45,48 @@ -3737,6 +3759,7 @@ mining pool + Pool de minage src/app/components/pool/pool-preview.component.html 3,5 @@ -4067,6 +4090,7 @@ Explore the full Bitcoin ecosystem + Explorez tout l'écosystème Bitcoin src/app/components/search-form/search-form.component.html 4,6 @@ -4427,6 +4451,7 @@ Flow + Flux src/app/components/transaction/transaction.component.html 195,198 @@ -4440,6 +4465,7 @@ Hide diagram + Masquer le diagramme src/app/components/transaction/transaction.component.html 198,203 @@ -4448,6 +4474,7 @@ Show more + Montrer plus src/app/components/transaction/transaction.component.html 219,221 @@ -4456,6 +4483,7 @@ Show less + Montrer moins src/app/components/transaction/transaction.component.html 221,227 @@ -4464,6 +4492,7 @@ Show diagram + Afficher le diagramme src/app/components/transaction/transaction.component.html 241,242 @@ -4657,6 +4686,7 @@ other inputs + autres entrées src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 12 @@ -4665,6 +4695,7 @@ other outputs + autres sorties src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 13 @@ -4673,6 +4704,7 @@ Input + Entrées src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 43 @@ -4681,6 +4713,7 @@ Output + Sorties src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 44 @@ -4689,6 +4722,7 @@ This transaction saved % on fees by using native SegWit + Cette transaction a permis d'économiser  % sur les frais en utilisant SegWit natif src/app/components/tx-features/tx-features.component.html 2 @@ -4715,6 +4749,7 @@ This transaction saved % on fees by using SegWit and could save % more by fully upgrading to native SegWit + Cette transaction a permis d'économiser  % sur les frais en utilisant SegWit et pourrait économiser  % de plus en passant entièrement à SegWit natif src/app/components/tx-features/tx-features.component.html 4 @@ -4723,6 +4758,7 @@ This transaction could save % on fees by upgrading to native SegWit or % by upgrading to SegWit-P2SH + Cette transaction pourrait économiser  % sur les frais en passant à SegWit natif ou  % en passant à SegWit-P2SH src/app/components/tx-features/tx-features.component.html 6 @@ -4731,6 +4767,7 @@ This transaction uses Taproot and thereby saved at least % on fees + Cette transaction utilise Taproot et a ainsi économisé au moins % sur les frais src/app/components/tx-features/tx-features.component.html 12 @@ -4739,6 +4776,7 @@ Taproot + Taproot src/app/components/tx-features/tx-features.component.html 12 @@ -4760,6 +4798,7 @@ This transaction uses Taproot and already saved at least % on fees, but could save an additional % by fully using Taproot + Cette transaction utilise Taproot et a déjà économisé au moins  % sur les frais, mais pourrait économiser  % supplémentaires en utilisant pleinement Taproot src/app/components/tx-features/tx-features.component.html 14 @@ -4768,6 +4807,7 @@ This transaction could save % on fees by using Taproot + Cette transaction pourrait économiser % sur les frais en utilisant Taproot src/app/components/tx-features/tx-features.component.html 16 @@ -4785,6 +4825,7 @@ This transaction supports Replace-By-Fee (RBF) allowing fee bumping + Cette transaction prend en charge le Replace-By-Fee/remplacement par frais (RBF), permettant une augmentation des frais src/app/components/tx-features/tx-features.component.html 25 @@ -5021,6 +5062,7 @@ Base fee + Frais de base src/app/lightning/channel/channel-box/channel-box.component.html 30 @@ -5033,6 +5075,7 @@ mSats + mSats src/app/lightning/channel/channel-box/channel-box.component.html 36 @@ -5049,6 +5092,7 @@ This channel supports zero base fee routing + Ce canal prend en charge le routage sans frais de base src/app/lightning/channel/channel-box/channel-box.component.html 45 @@ -5057,6 +5101,7 @@ Zero base fee + Aucun frais de base src/app/lightning/channel/channel-box/channel-box.component.html 46 @@ -5065,6 +5110,7 @@ This channel does not support zero base fee routing + Ce canal ne prend pas en charge le routage sans frais de base src/app/lightning/channel/channel-box/channel-box.component.html 51 @@ -5073,6 +5119,7 @@ Non-zero base fee + Frais de base non nuls src/app/lightning/channel/channel-box/channel-box.component.html 52 @@ -5081,6 +5128,7 @@ Min HTLC + HTLC min. src/app/lightning/channel/channel-box/channel-box.component.html 58 @@ -5089,6 +5137,7 @@ Max HTLC + HTLC max. src/app/lightning/channel/channel-box/channel-box.component.html 64 @@ -5097,6 +5146,7 @@ Timelock delta + Timelock delta src/app/lightning/channel/channel-box/channel-box.component.html 70 @@ -5105,6 +5155,7 @@ channels + canaux src/app/lightning/channel/channel-box/channel-box.component.html 80 @@ -5117,6 +5168,7 @@ lightning channel + canal Lightning src/app/lightning/channel/channel-preview.component.html 3,5 @@ -5125,6 +5177,7 @@ Inactive + Inactif src/app/lightning/channel/channel-preview.component.html 10,11 @@ -5141,6 +5194,7 @@ Active + Actif src/app/lightning/channel/channel-preview.component.html 11,12 @@ -5157,6 +5211,7 @@ Closed + Fermé src/app/lightning/channel/channel-preview.component.html 12,14 @@ -5177,6 +5232,7 @@ Created + Établi src/app/lightning/channel/channel-preview.component.html 23,26 @@ -5189,6 +5245,7 @@ Capacity + Capacité src/app/lightning/channel/channel-preview.component.html 27,28 @@ -5241,6 +5298,7 @@ ppm + ppm src/app/lightning/channel/channel-preview.component.html 34,35 @@ -5261,6 +5319,7 @@ Lightning channel + Canal Lightning src/app/lightning/channel/channel.component.html 2,5 @@ -5273,6 +5332,7 @@ Last update + Dernière mise à jour src/app/lightning/channel/channel.component.html 33,34 @@ -5305,6 +5365,7 @@ Closing date + Date de clôture src/app/lightning/channel/channel.component.html 37,38 @@ -5317,6 +5378,7 @@ Opening transaction + Transaction d'ouverture src/app/lightning/channel/channel.component.html 73,74 @@ -5325,6 +5387,7 @@ Closing transaction + Transaction de clôture src/app/lightning/channel/channel.component.html 82,84 @@ -5333,6 +5396,7 @@ Channel: + Canal : src/app/lightning/channel/channel.component.ts 37 @@ -5340,6 +5404,7 @@ Open + Ouvert src/app/lightning/channels-list/channels-list.component.html 5,7 @@ -5348,6 +5413,7 @@ No channels to display + Aucun canal à afficher src/app/lightning/channels-list/channels-list.component.html 29,35 @@ -5356,6 +5422,7 @@ Alias + Alias src/app/lightning/channels-list/channels-list.component.html 35,37 @@ -5392,6 +5459,7 @@ Status + Statut src/app/lightning/channels-list/channels-list.component.html 37,38 @@ -5400,6 +5468,7 @@ Channel ID + ID du canal src/app/lightning/channels-list/channels-list.component.html 41,45 @@ -5408,6 +5477,7 @@ sats + sats src/app/lightning/channels-list/channels-list.component.html 61,65 @@ -5460,6 +5530,7 @@ Avg Capacity + Capacité moy src/app/lightning/channels-statistics/channels-statistics.component.html 13,15 @@ -5472,6 +5543,7 @@ Avg Fee Rate + Taux de frais moy src/app/lightning/channels-statistics/channels-statistics.component.html 26,28 @@ -5484,6 +5556,7 @@ The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Le taux de frais moyen facturé par les nœuds de routage, en ignorant les taux de frais > 0,5 % ou 5 000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 28,30 @@ -5492,6 +5565,7 @@ Avg Base Fee + Frais de base moy src/app/lightning/channels-statistics/channels-statistics.component.html 41,43 @@ -5504,6 +5578,7 @@ The average base fee charged by routing nodes, ignoring base fees > 5000ppm + Frais de base moyens facturés par les nœuds de routage, sans tenir compte des frais de base > 5 000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 43,45 @@ -5512,6 +5587,7 @@ Med Capacity + Capacité med src/app/lightning/channels-statistics/channels-statistics.component.html 59,61 @@ -5520,6 +5596,7 @@ Med Fee Rate + Taux de frais med src/app/lightning/channels-statistics/channels-statistics.component.html 72,74 @@ -5528,6 +5605,7 @@ The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Le taux de frais médian facturé par les nœuds de routage, en ignorant les taux de frais > 0,5 % ou 5 000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 74,76 @@ -5536,6 +5614,7 @@ Med Base Fee + Frais de base med src/app/lightning/channels-statistics/channels-statistics.component.html 87,89 @@ -5544,6 +5623,7 @@ The median base fee charged by routing nodes, ignoring base fees > 5000ppm + Frais de base médians facturés par les nœuds de routage, sans tenir compte des frais de base > 5 000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 89,91 @@ -5552,6 +5632,7 @@ Lightning node group + Groupe de nœuds Lightning src/app/lightning/group/group-preview.component.html 3,5 @@ -5564,6 +5645,7 @@ Nodes + Nœuds src/app/lightning/group/group-preview.component.html 25,29 @@ -5604,6 +5686,7 @@ Liquidity + Liquidité src/app/lightning/group/group-preview.component.html 29,31 @@ -5640,6 +5723,7 @@ Channels + Canaux src/app/lightning/group/group-preview.component.html 40,43 @@ -5700,6 +5784,7 @@ Average size + Taille moyenne src/app/lightning/group/group-preview.component.html 44,46 @@ -5712,6 +5797,7 @@ Location + Emplacement src/app/lightning/group/group.component.html 74,77 @@ -5752,6 +5838,7 @@ Network Statistics + Statistiques du réseau src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 10 @@ -5760,6 +5847,7 @@ Channels Statistics + Statistiques des canaux src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 24 @@ -5768,6 +5856,7 @@ Lightning Network History + Historique du réseau Lightning src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 49 @@ -5776,6 +5865,7 @@ Liquidity Ranking + Classement par liquidité src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 62 @@ -5792,6 +5882,7 @@ Connectivity Ranking + Classement par connectivité src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 76 @@ -5804,6 +5895,7 @@ Percentage change past week + Variation sur une semaine src/app/lightning/node-statistics/node-statistics.component.html 5,7 @@ -5820,6 +5912,7 @@ Lightning node + Nœud Lightning src/app/lightning/node/node-preview.component.html 3,5 @@ -5836,6 +5929,7 @@ Active capacity + Capacité active src/app/lightning/node/node-preview.component.html 20,22 @@ -5848,6 +5942,7 @@ Active channels + Canaux actifs src/app/lightning/node/node-preview.component.html 26,30 @@ -5860,6 +5955,7 @@ Country + Pays src/app/lightning/node/node-preview.component.html 44,47 @@ -5868,6 +5964,7 @@ No node found for public key "" + Aucun nœud trouvé pour la clé publique &quot; &quot; src/app/lightning/node/node.component.html 17,19 @@ -5876,6 +5973,7 @@ Average channel size + Taille moyenne du canal src/app/lightning/node/node.component.html 40,43 @@ -5884,6 +5982,7 @@ Unknown + Inconnue src/app/lightning/node/node.component.html 52,56 @@ -5904,6 +6003,7 @@ Color + Couleur src/app/lightning/node/node.component.html 75,77 @@ -5912,6 +6012,7 @@ ISP + FAI src/app/lightning/node/node.component.html 82,83 @@ -5924,6 +6025,7 @@ Exclusively on Tor + Exclusivement sur Tor src/app/lightning/node/node.component.html 88,90 @@ -5932,6 +6034,7 @@ Open channels + Canaux ouverts src/app/lightning/node/node.component.html 145,148 @@ -5940,6 +6043,7 @@ Closed channels + Canaux fermés src/app/lightning/node/node.component.html 149,152 @@ -5948,6 +6052,7 @@ Node: + Nœud : src/app/lightning/node/node.component.ts 42 @@ -5955,6 +6060,7 @@ (Tor nodes excluded) + (Nœuds Tor exclus) src/app/lightning/nodes-channels-map/nodes-channels-map.component.html 8,11 @@ -5975,6 +6081,7 @@ Lightning Nodes Channels World Map + Carte du monde des canaux Lightning src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 69 @@ -5982,6 +6089,7 @@ No geolocation data available + Aucune donnée de géolocalisation disponible src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 218,213 @@ -5989,6 +6097,7 @@ Active channels map + Carte des canaux actifs src/app/lightning/nodes-channels/node-channels.component.html 2,3 @@ -5997,6 +6106,7 @@ Indexing in progess + Indexation en cours src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 121,116 @@ -6008,6 +6118,7 @@ Reachable on Clearnet Only + Accessible uniquement sur Clearnet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 164,161 @@ -6019,6 +6130,7 @@ Reachable on Clearnet and Darknet + Accessible sur Clearnet et Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 185,182 @@ -6030,6 +6142,7 @@ Reachable on Darknet Only + Accessible uniquement sur Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 206,203 @@ -6041,6 +6154,7 @@ Share + Part src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html 29,31 @@ -6053,6 +6167,7 @@ nodes + nœuds src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 103,102 @@ -6068,6 +6183,7 @@ BTC capacity + Capacité de BTC src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 104,102 @@ -6075,6 +6191,7 @@ Lightning nodes in + Nœuds Lightning: src/app/lightning/nodes-per-country/nodes-per-country.component.html 3,4 @@ -6083,6 +6200,7 @@ ISP Count + Nombre de FAI src/app/lightning/nodes-per-country/nodes-per-country.component.html 34,38 @@ -6091,6 +6209,7 @@ Top ISP + FAI les plus utilisés src/app/lightning/nodes-per-country/nodes-per-country.component.html 38,40 @@ -6099,6 +6218,7 @@ Lightning nodes in + Nœuds Lightning: src/app/lightning/nodes-per-country/nodes-per-country.component.ts 35 @@ -6106,6 +6226,7 @@ Clearnet Capacity + Capacité Clearnet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 6,8 @@ -6118,6 +6239,7 @@ How much liquidity is running on nodes advertising at least one clearnet IP address + Liquidité qui circule sur les nœuds annonçant au moins une adresse IP clearnet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 8,9 @@ -6126,6 +6248,7 @@ Unknown Capacity + Capacité inconnue src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 13,15 @@ -6138,6 +6261,7 @@ How much liquidity is running on nodes which ISP was not identifiable + Liquidité qui circule sur les nœuds dont le FAI n'était pas identifiable src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 15,16 @@ -6146,6 +6270,7 @@ Tor Capacity + Capacité Tor src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 20,22 @@ -6158,6 +6283,7 @@ How much liquidity is running on nodes advertising only Tor addresses + Liquidité qui circule sur les nœuds annonçant uniquement les adresses Tor src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 22,23 @@ -6166,6 +6292,7 @@ Top 100 ISPs hosting LN nodes + Top 100 des FAI hébergeant des nœuds LN src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 31,33 @@ -6174,6 +6301,7 @@ BTC + BTC src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 158,156 @@ -6185,6 +6313,7 @@ Lightning ISP + FAI Lightning src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 3,5 @@ -6193,6 +6322,7 @@ Top country + Meilleur pays src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 39,41 @@ -6205,6 +6335,7 @@ Top node + Meilleur nœud src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 45,48 @@ -6213,6 +6344,7 @@ Lightning nodes on ISP: [AS] + Nœuds Lightning sur le FAI : [AS ] src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 44 @@ -6224,6 +6356,7 @@ Lightning nodes on ISP: + Nœuds Lightning sur le FAI : src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 2,4 @@ -6232,6 +6365,7 @@ ASN + ASN src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 11,14 @@ -6240,6 +6374,7 @@ Top 100 oldest lightning nodes + Top 100 des nœuds Lightning les plus anciens src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html 3,7 @@ -6248,6 +6383,7 @@ Oldest lightning nodes + Noeuds Lightning les plus anciens src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 27 @@ -6255,6 +6391,7 @@ Top 100 nodes liquidity ranking + Classement des 100 meilleurs nœuds par liquidité src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html 3,7 @@ -6263,6 +6400,7 @@ Top 100 nodes connectivity ranking + Classement des 100 meilleurs nœuds par connectivité src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html 3,7 @@ -6271,6 +6409,7 @@ Oldest nodes + Nœuds les plus anciens src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html 36 @@ -6279,6 +6418,7 @@ Top lightning nodes + Top nœuds Lightning src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 22 @@ -6286,6 +6426,7 @@ Indexing in progress + Indexation en cours src/app/lightning/statistics-chart/lightning-statistics-chart.component.html 52,55 diff --git a/frontend/src/locale/messages.lt.xlf b/frontend/src/locale/messages.lt.xlf index 3ba552f06..2eaf6764b 100644 --- a/frontend/src/locale/messages.lt.xlf +++ b/frontend/src/locale/messages.lt.xlf @@ -331,7 +331,7 @@ Balance - Balansas + Likutis src/app/bisq/bisq-address/bisq-address.component.html 30 @@ -348,7 +348,7 @@ transaction - pervedimas + operacija src/app/bisq/bisq-address/bisq-address.component.html 50 @@ -373,7 +373,7 @@ transactions - pervedimai + operacijos src/app/bisq/bisq-address/bisq-address.component.html 51 @@ -554,7 +554,7 @@ Transactions - Transakcijos + Operacijos src/app/bisq/bisq-blocks/bisq-blocks.component.html 15,18 @@ -615,7 +615,7 @@ Bisq Trading Volume - Bisq Prekybos Apimtis + Bisq Mainų Apimtis src/app/bisq/bisq-dashboard/bisq-dashboard.component.html 3,7 @@ -733,7 +733,7 @@ Bisq Price Index - Bisq Kainų Indeksas + Bisq Kainos Indeksas src/app/bisq/bisq-main-dashboard/bisq-main-dashboard.component.html 9,11 @@ -820,7 +820,7 @@ Buy Offers - Pirkimo Pasiūlymai + Perka src/app/bisq/bisq-market/bisq-market.component.html 73,74 @@ -829,7 +829,7 @@ Sell Offers - Pardavimo Pasiūlymai + Parduoda src/app/bisq/bisq-market/bisq-market.component.html 74,77 @@ -851,7 +851,7 @@ BSQ statistics - BSQ Statistika + BSQ statistika src/app/bisq/bisq-stats/bisq-stats.component.html 2 @@ -940,7 +940,7 @@ Unspent TXOs - Nepanaudos Išeigos + Nepanaudotos išvestys src/app/bisq/bisq-stats/bisq-stats.component.html 28 @@ -957,7 +957,7 @@ Spent TXOs - Panaudotos Išeigos + Panaudotos Išvestys src/app/bisq/bisq-stats/bisq-stats.component.html 32 @@ -1016,7 +1016,7 @@ Outputs - Išeigos + Išvestys src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html 11 @@ -1039,7 +1039,7 @@ Type - Tipas + Rūšis src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html 25 @@ -1076,7 +1076,7 @@ Transaction - Transakcija + Operacija src/app/bisq/bisq-transaction/bisq-transaction.component.html 6,10 @@ -1208,7 +1208,7 @@ Inputs & Outputs - Įvestys ir Išeigos + Įvestys ir Išvestys src/app/bisq/bisq-transaction/bisq-transaction.component.html 97,105 @@ -1230,7 +1230,7 @@ Transaction: - Transakcija: + Operacija: src/app/bisq/bisq-transaction/bisq-transaction.component.ts 50 @@ -1246,7 +1246,7 @@ BSQ Transactions - BSQ Transakcijos + BSQ Operacijos src/app/bisq/bisq-transactions/bisq-transactions.component.html 2,5 @@ -1284,7 +1284,7 @@ Asset listing fee - Turto įtraukimo į sąrašą mokestis + Įtraukimo į sąrašą mokestis src/app/bisq/bisq-transactions/bisq-transactions.component.ts 31 @@ -1340,7 +1340,7 @@ Pay trade fee - Mokėti prekybos mokestį + Mokėti mainų mokestį src/app/bisq/bisq-transactions/bisq-transactions.component.ts 37 @@ -1388,7 +1388,7 @@ Vote reveal - Balsavimo atskleidimas + Rodyti balsus src/app/bisq/bisq-transactions/bisq-transactions.component.ts 43 @@ -1445,6 +1445,7 @@ Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties. + Mūsų Bitkoino bendruomenei skirta atminties ir blokų grandinės naršyklė, daugiausia dėmesio skirianti mokesčių rinkai ir daugiasluoksnei ekosistemai, savarankiškai leidžiama be jokių patikimų trečiųjų šalių. src/app/components/about/about.component.html 13,17 @@ -1452,6 +1453,7 @@ Enterprise Sponsors 🚀 + Verslo Rėmėjai 🚀 src/app/components/about/about.component.html 29,32 @@ -1460,6 +1462,7 @@ Community Sponsors ❤️ + Bendruomenės Rėmėjai ❤️ src/app/components/about/about.component.html 177,180 @@ -1468,6 +1471,7 @@ Community Integrations + Bendruomenės Integracijos src/app/components/about/about.component.html 191,193 @@ -1476,6 +1480,7 @@ Community Alliances + Bendruomenės Aljansai src/app/components/about/about.component.html 281,283 @@ -1484,6 +1489,7 @@ Project Translators + Projekto Vertėjai src/app/components/about/about.component.html 297,299 @@ -1492,6 +1498,7 @@ Project Contributors + Projekto Pagalbininkai src/app/components/about/about.component.html 311,313 @@ -1500,6 +1507,7 @@ Project Members + Projekto Nariai src/app/components/about/about.component.html 323,325 @@ -1508,6 +1516,7 @@ Project Maintainers + Projekto Prižiūrėtojai src/app/components/about/about.component.html 336,338 @@ -1516,6 +1525,7 @@ About + Apie src/app/components/about/about.component.ts 39 @@ -1535,6 +1545,7 @@ Multisig of + Multiparašas src/app/components/address-labels/address-labels.component.ts 105 @@ -1542,6 +1553,7 @@ Unconfidential + Nekonfidencialu src/app/components/address/address-preview.component.html 15 @@ -1554,6 +1566,7 @@ Confidential + Konfidencialu src/app/components/address/address-preview.component.html 56 @@ -1594,6 +1607,7 @@ Address: + Adresas: src/app/components/address/address-preview.component.ts 70 @@ -1605,6 +1619,7 @@ of transaction + operacijos src/app/components/address/address.component.html 60 @@ -1613,6 +1628,7 @@ of transactions + operacijų src/app/components/address/address.component.html 61 @@ -1621,6 +1637,7 @@ Error loading address data. + Įkeliant adreso duomenis įvyko klaida. src/app/components/address/address.component.html 130 @@ -1629,6 +1646,7 @@ There many transactions on this address, more than your backend can handle. See more on setting up a stronger backend. Consider viewing this address on the official Mempool website instead: + Šiuo adresu yra daug operacijų, daugiau nei gali parodyti jūsų vidinė sistema. Žr. daugiau apie , kaip nustatyti galingesnę vidinę sistemą . Apsvarstykite galimybę peržiūrėti šį adresą oficialioje „Mempool“ svetainėje: src/app/components/address/address.component.html 135,138 @@ -1637,6 +1655,7 @@ Asset + Turtas src/app/components/asset/asset.component.html 3 @@ -1646,6 +1665,7 @@ Name + Pavadinimas src/app/components/asset/asset.component.html 23 @@ -1666,6 +1686,7 @@ Precision + Tikslumas src/app/components/asset/asset.component.html 27 @@ -1675,6 +1696,7 @@ Issuer + Emitentas src/app/components/asset/asset.component.html 31 @@ -1684,6 +1706,7 @@ Issuance TX + Išdavimo TX src/app/components/asset/asset.component.html 35 @@ -1693,6 +1716,7 @@ Pegged in + Prisegta src/app/components/asset/asset.component.html 39 @@ -1702,6 +1726,7 @@ Pegged out + Išsegta src/app/components/asset/asset.component.html 43 @@ -1711,6 +1736,7 @@ Burned amount + Sudeginta src/app/components/asset/asset.component.html 51 @@ -1720,6 +1746,7 @@ Circulating amount + Apyvartoje cirkuliuojantis kiekis src/app/components/asset/asset.component.html 55 @@ -1733,6 +1760,7 @@ of   + src/app/components/asset/asset.component.html 80 @@ -1741,6 +1769,7 @@ Peg In/Out and Burn Transactions + Prisegimo/išsegimo ir deginimo operacijos src/app/components/asset/asset.component.html 81 @@ -1749,6 +1778,7 @@ Issuance and Burn Transactions + Išdavimo ir deginimo operacijos src/app/components/asset/asset.component.html 82 @@ -1757,6 +1787,7 @@ Error loading asset data. + Įkeliant lėšų duomenis įvyko klaida. src/app/components/asset/asset.component.html 152 @@ -1765,6 +1796,7 @@ Asset: + Lėšos: src/app/components/asset/asset.component.ts 75 @@ -1772,6 +1804,7 @@ Group of assets + lėšų grupė src/app/components/assets/asset-group/asset-group.component.html 8,9 @@ -1783,6 +1816,7 @@ Assets + Lėšos src/app/components/assets/assets-nav/assets-nav.component.html 3 @@ -1803,6 +1837,7 @@ Featured + Rodomas src/app/components/assets/assets-nav/assets-nav.component.html 9 @@ -1810,6 +1845,7 @@ All + Visi src/app/components/assets/assets-nav/assets-nav.component.html 13 @@ -1837,6 +1873,7 @@ Search asset + Ieškoti lėšų src/app/components/assets/assets-nav/assets-nav.component.html 19 @@ -1845,6 +1882,7 @@ Clear + Išvalyti src/app/components/assets/assets-nav/assets-nav.component.html 21 @@ -1853,6 +1891,7 @@ Ticker + Tikeris src/app/components/assets/assets.component.html 5,6 @@ -1865,6 +1904,7 @@ Issuer domain + Išdavėjo domenas src/app/components/assets/assets.component.html 6,9 @@ -1877,6 +1917,7 @@ Asset ID + Lėšų ID src/app/components/assets/assets.component.html 7,10 @@ -1889,6 +1930,7 @@ Error loading assets data. + Įkeliant lėšų duomenis įvyko klaida. src/app/components/assets/assets.component.html 48,53 @@ -1897,6 +1939,7 @@ Offline + Neprisijungęs src/app/components/bisq-master-page/bisq-master-page.component.html 36,37 @@ -1913,6 +1956,7 @@ Reconnecting... + Jungiamasi iš naujo... src/app/components/bisq-master-page/bisq-master-page.component.html 37,42 @@ -1929,6 +1973,7 @@ Layer 2 Networks + 2-tro Sluoksnio Tinklai src/app/components/bisq-master-page/bisq-master-page.component.html 50,51 @@ -1945,6 +1990,7 @@ Dashboard + Valdymo skydas src/app/components/bisq-master-page/bisq-master-page.component.html 60,62 @@ -1961,6 +2007,7 @@ Stats + Statistika src/app/components/bisq-master-page/bisq-master-page.component.html 69,71 @@ -1969,6 +2016,7 @@ Docs + Dokumentai src/app/components/bisq-master-page/bisq-master-page.component.html 72,74 @@ -1981,6 +2029,7 @@ Block + Blokas src/app/components/block-audit/block-audit.component.html 7,9 @@ -1989,6 +2038,7 @@ Template vs Mined + Šablonas vs Iškasimą src/app/components/block-audit/block-audit.component.html 11,17 @@ -1997,6 +2047,7 @@ Size + Dydis src/app/components/block-audit/block-audit.component.html 44,46 @@ -2045,6 +2096,7 @@ Weight + Svoris src/app/components/block-audit/block-audit.component.html 48,49 @@ -2073,6 +2125,7 @@ Match rate + Atitikties rodiklis src/app/components/block-audit/block-audit.component.html 64,67 @@ -2081,6 +2134,7 @@ Missing txs + Trūkstamos txs src/app/components/block-audit/block-audit.component.html 68,71 @@ -2089,6 +2143,7 @@ Added txs + Pridėtos txs src/app/components/block-audit/block-audit.component.html 72,75 @@ -2097,6 +2152,7 @@ Missing + Trūksta src/app/components/block-audit/block-audit.component.html 84,85 @@ -2105,6 +2161,7 @@ Added + Pridėta src/app/components/block-audit/block-audit.component.html 86,92 @@ -2113,6 +2170,7 @@ Block Fee Rates + Bloko Mokesčių Tarifai src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html 6,8 @@ -2129,6 +2187,7 @@ At block: + Bloke: src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 188 @@ -2144,6 +2203,7 @@ Around block: + Aplink bloką: src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 190 @@ -2159,6 +2219,7 @@ Block Fees + Bloko Mokesčiai src/app/components/block-fees-graph/block-fees-graph.component.html 6,7 @@ -2175,6 +2236,7 @@ Indexing blocks + Indeksavimo blokai src/app/components/block-fees-graph/block-fees-graph.component.ts 110,105 @@ -2210,6 +2272,7 @@ Fee + Mokestis src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 22 @@ -2235,6 +2298,7 @@ sat + sat src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 23 @@ -2256,6 +2320,7 @@ Fee rate + Mokesčio tarifas src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 26 @@ -2285,6 +2350,7 @@ sat/vB + sat/vB src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 28 @@ -2378,6 +2444,7 @@ Virtual size + Virtualus dydis src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 32 @@ -2395,6 +2462,7 @@ Block Prediction Accuracy + Blokų Numatymo Tikslumas src/app/components/block-prediction-graph/block-prediction-graph.component.html 6,8 @@ -2411,6 +2479,7 @@ No data to display yet. Try again later. + Dar nėra duomenų, kuriuos būtų galima rodyti. Bandyk dar kartą vėliau. src/app/components/block-prediction-graph/block-prediction-graph.component.ts 108,103 @@ -2422,6 +2491,7 @@ Match rate + Atitikties rodiklis src/app/components/block-prediction-graph/block-prediction-graph.component.ts 189,187 @@ -2429,6 +2499,7 @@ Block Rewards + Bloko Atlygis src/app/components/block-rewards-graph/block-rewards-graph.component.html 7,8 @@ -2445,6 +2516,7 @@ Block Sizes and Weights + Blokų Dydžiai ir Svoriai src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html 5,7 @@ -2461,6 +2533,7 @@ Block + Blokas src/app/components/block/block-preview.component.html 3,7 @@ -2473,6 +2546,7 @@ + src/app/components/block/block-preview.component.html 11,12 @@ -2481,6 +2555,7 @@ Median fee + Vidutinis mokestis src/app/components/block/block-preview.component.html 36,37 @@ -2501,6 +2576,7 @@ Total fees + Bendri mokesčiai src/app/components/block/block-preview.component.html 41,43 @@ -2530,6 +2606,7 @@ Miner + Radėjas src/app/components/block/block-preview.component.html 53,55 @@ -2546,6 +2623,7 @@ Block : + Blokas : src/app/components/block/block-preview.component.ts 98 @@ -2557,6 +2635,7 @@ Next Block + Kitas Blokas src/app/components/block/block.component.html 8,9 @@ -2569,6 +2648,7 @@ Previous Block + Ankstesnis Blokas src/app/components/block/block.component.html 15,16 @@ -2577,6 +2657,7 @@ Based on average native segwit transaction of 140 vBytes + Remiantis vidutine 140 vBaitų 'native segwit' operacija src/app/components/block/block.component.html 60,62 @@ -2609,6 +2690,7 @@ Subsidy + fees: + Subsidija + mokesčiai: src/app/components/block/block.component.html 79,81 @@ -2630,6 +2712,7 @@ Bits + Bitai src/app/components/block/block.component.html 256,258 @@ -2638,6 +2721,7 @@ Merkle root + Merkle šaknis src/app/components/block/block.component.html 260,262 @@ -2646,6 +2730,7 @@ Difficulty + Sudėtingumas src/app/components/block/block.component.html 270,273 @@ -2674,6 +2759,7 @@ Nonce + Noncas src/app/components/block/block.component.html 274,276 @@ -2682,6 +2768,7 @@ Block Header Hex + Bloko Antraštės Hex src/app/components/block/block.component.html 278,279 @@ -2690,6 +2777,7 @@ Details + Detalės src/app/components/block/block.component.html 289,293 @@ -2711,6 +2799,7 @@ Error loading data. + Įkeliant duomenis įvyko klaida. src/app/components/block/block.component.html 308,310 @@ -2739,6 +2828,7 @@ Pool + Telkinys src/app/components/blocks-list/blocks-list.component.html 14 @@ -2759,6 +2849,7 @@ Mined + Rasta src/app/components/blocks-list/blocks-list.component.html 16,17 @@ -2779,6 +2870,7 @@ Reward + Atlygis src/app/components/blocks-list/blocks-list.component.html 18,19 @@ -2799,6 +2891,7 @@ Fees + Mokesčiai src/app/components/blocks-list/blocks-list.component.html 19,20 @@ -2815,6 +2908,7 @@ TXs + TXs src/app/components/blocks-list/blocks-list.component.html 21,22 @@ -2843,6 +2937,7 @@ Copied! + Nukopijuota! src/app/components/clipboard/clipboard.component.ts 19 @@ -2850,6 +2945,7 @@ Adjusted + Pakoreguota src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html 6,8 @@ -2858,6 +2954,7 @@ Change + Pokytis src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html 8,11 @@ -2866,6 +2963,7 @@ Difficulty Adjustment + Sudėtingumo Pokytis src/app/components/difficulty/difficulty.component.html 1,5 @@ -2878,6 +2976,7 @@ Remaining + Likę src/app/components/difficulty/difficulty.component.html 7,9 @@ -2890,6 +2989,7 @@ blocks + blokai src/app/components/difficulty/difficulty.component.html 10,11 @@ -2914,6 +3014,7 @@ block + blokas src/app/components/difficulty/difficulty.component.html 11,12 @@ -2930,6 +3031,7 @@ Estimate + Tikimasi src/app/components/difficulty/difficulty.component.html 16,17 @@ -2942,6 +3044,7 @@ Previous + Ankstesnis src/app/components/difficulty/difficulty.component.html 31,33 @@ -2950,6 +3053,7 @@ Current Period + Einamasis Periodas src/app/components/difficulty/difficulty.component.html 43,44 @@ -2962,6 +3066,7 @@ Next Halving + Kitas Blokų Atlygio Dalijimas src/app/components/difficulty/difficulty.component.html 50,52 @@ -2970,6 +3075,7 @@ Either 2x the minimum, or the Low Priority rate (whichever is lower) + Arba 2 kart daugiau už minimalų, arba Žemo Prioriteto tarifas (atsižvelgiant į tai, kuris mažesnis) src/app/components/fees-box/fees-box.component.html 4,7 @@ -2978,6 +3084,7 @@ No Priority + Nėra Prioriteto src/app/components/fees-box/fees-box.component.html 4,7 @@ -2990,6 +3097,7 @@ Usually places your transaction in between the second and third mempool blocks + Paprastai įkelia jūsų operacija tarp antrojo ir trečiojo atminties blokų src/app/components/fees-box/fees-box.component.html 8,9 @@ -2998,6 +3106,7 @@ Low Priority + Žemas Prioritetas src/app/components/fees-box/fees-box.component.html 8,9 @@ -3010,6 +3119,7 @@ Usually places your transaction in between the first and second mempool blocks + Paprastai įkelia jūsų operacija yra tarp pirmojo ir antrojo atminties blokų src/app/components/fees-box/fees-box.component.html 9,10 @@ -3018,6 +3128,7 @@ Medium Priority + Vidutinis Prioritetas src/app/components/fees-box/fees-box.component.html 9,10 @@ -3030,6 +3141,7 @@ Places your transaction in the first mempool block + Įkelia jūsų operaciją į pirmąjį atminties bloką src/app/components/fees-box/fees-box.component.html 10,14 @@ -3038,6 +3150,7 @@ High Priority + Didelis Prioritetas src/app/components/fees-box/fees-box.component.html 10,15 @@ -3050,6 +3163,7 @@ Incoming transactions + Įeinančios operacijos src/app/components/footer/footer.component.html 5,6 @@ -3062,6 +3176,7 @@ Backend is synchronizing + Vidinė sistema sinchronizuojasi src/app/components/footer/footer.component.html 8,10 @@ -3074,6 +3189,7 @@ vB/s + vB/s src/app/components/footer/footer.component.html 13,17 @@ -3087,6 +3203,7 @@ Unconfirmed + Nepatvirtinta src/app/components/footer/footer.component.html 19,21 @@ -3100,6 +3217,7 @@ Mempool size + Mempool dydis src/app/components/footer/footer.component.html 23,24 @@ -3109,6 +3227,7 @@ Mining + Gavyba src/app/components/graphs/graphs.component.html 8 @@ -3117,6 +3236,7 @@ Pools Ranking + Baseinų Reitingas src/app/components/graphs/graphs.component.html 11 @@ -3129,6 +3249,7 @@ Pools Dominance + Baseinų dominavimas src/app/components/graphs/graphs.component.html 13 @@ -3141,6 +3262,7 @@ Hashrate & Difficulty + Maišos Dažnis ir Sudėtingumas src/app/components/graphs/graphs.component.html 15,16 @@ -3149,6 +3271,7 @@ Lightning + Žaibatinklis src/app/components/graphs/graphs.component.html 31 @@ -3157,6 +3280,7 @@ Lightning Nodes Per Network + Žaibatinklio Mazgai Tinkle src/app/components/graphs/graphs.component.html 34 @@ -3177,6 +3301,7 @@ Lightning Network Capacity + Žaibo Tinklo Talpumas src/app/components/graphs/graphs.component.html 36 @@ -3197,6 +3322,7 @@ Lightning Nodes Per ISP + Žaibatinklio Mazgai Tenkantys Vienam IPT src/app/components/graphs/graphs.component.html 38 @@ -3209,6 +3335,7 @@ Lightning Nodes Per Country + Žaibatinklio Mazgai Tenkantys Vienai Šaliai src/app/components/graphs/graphs.component.html 40 @@ -3225,6 +3352,7 @@ Lightning Nodes World Map + Žaibatinklio Mazgų Pasaulio Žemėlapis src/app/components/graphs/graphs.component.html 42 @@ -3241,6 +3369,7 @@ Lightning Nodes Channels World Map + Žaibatinklio Kanalų Pasaulio Žemėlapis src/app/components/graphs/graphs.component.html 44 @@ -3253,6 +3382,7 @@ Hashrate + Maišos Dažnis src/app/components/hashrate-chart/hashrate-chart.component.html 8,10 @@ -3281,6 +3411,7 @@ Hashrate & Difficulty + Maišos Dažnis ir Sudėtingumas src/app/components/hashrate-chart/hashrate-chart.component.html 27,29 @@ -3293,6 +3424,7 @@ Hashrate (MA) + Maišos dažnis (JV) src/app/components/hashrate-chart/hashrate-chart.component.ts 292,291 @@ -3304,6 +3436,7 @@ Pools Historical Dominance + Telkinių Istorinis Dominavimas src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts 64 @@ -3311,6 +3444,7 @@ Indexing network hashrate + Tinklo maišos dažnio indeksavimas src/app/components/indexing-progress/indexing-progress.component.html 2 @@ -3318,6 +3452,7 @@ Indexing pools hashrate + Telkinių maišos dažnio indeksavimas src/app/components/indexing-progress/indexing-progress.component.html 3 @@ -3325,6 +3460,7 @@ Graphs + Grafikai src/app/components/liquid-master-page/liquid-master-page.component.html 71,74 @@ -3341,6 +3477,7 @@ Mining Dashboard + Gavybos Valdymo Skydas src/app/components/master-page/master-page.component.html 41,43 @@ -3353,6 +3490,7 @@ Lightning Explorer + Žaibatinklio Naršyklė src/app/components/master-page/master-page.component.html 44,45 @@ -3365,6 +3503,7 @@ beta + beta versija src/app/components/master-page/master-page.component.html 45,48 @@ -3373,6 +3512,7 @@ Documentation + Dokumentacija src/app/components/master-page/master-page.component.html 55,57 @@ -3385,6 +3525,7 @@ Fee span + Mokesčio intervalas src/app/components/mempool-block/mempool-block.component.html 20,21 @@ -3393,6 +3534,7 @@ Stack of mempool blocks + mempool blokų krūva src/app/components/mempool-block/mempool-block.component.ts 77 @@ -3400,6 +3542,7 @@ Mempool block + Mempool blokas src/app/components/mempool-block/mempool-block.component.ts 79 @@ -3407,6 +3550,7 @@ Range + Intervalas src/app/components/mempool-graph/mempool-graph.component.ts 259 @@ -3414,6 +3558,7 @@ Sum + Suma src/app/components/mempool-graph/mempool-graph.component.ts 261 @@ -3421,6 +3566,7 @@ Reward stats + Atlygio statistika src/app/components/mining-dashboard/mining-dashboard.component.html 10 @@ -3429,6 +3575,7 @@ (144 blocks) + (144 blokai) src/app/components/mining-dashboard/mining-dashboard.component.html 11 @@ -3437,6 +3584,7 @@ Latest blocks + Naujausi blokai src/app/components/mining-dashboard/mining-dashboard.component.html 53 @@ -3449,6 +3597,7 @@ Adjustments + Koregavimai src/app/components/mining-dashboard/mining-dashboard.component.html 67 @@ -3457,6 +3606,7 @@ Pools luck (1 week) + Telkinių sėkmė (1 savaitė) src/app/components/pool-ranking/pool-ranking.component.html 9 @@ -3465,6 +3615,7 @@ Pools luck + Telkinių sėkmė src/app/components/pool-ranking/pool-ranking.component.html 9,11 @@ -3473,6 +3624,7 @@ The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes. + Bendra visų gavybos telkinių sėkmė per praėjusią savaitę. Sėkmė, didesnė nei 100 %, reiškia, kad vidutinis einamosios epochos bloko laikas yra trumpesnis nei 10 minučių. src/app/components/pool-ranking/pool-ranking.component.html 11,15 @@ -3481,6 +3633,7 @@ Pools count (1w) + Suskaičiuota telkinių (1 sav.) src/app/components/pool-ranking/pool-ranking.component.html 17 @@ -3489,6 +3642,7 @@ Pools count + Suskaičiuota telkinių src/app/components/pool-ranking/pool-ranking.component.html 17,19 @@ -3497,6 +3651,7 @@ How many unique pools found at least one block over the past week. + Kiek unikalių telkinių rado bent vieną bloką per pastarąją savaitę. src/app/components/pool-ranking/pool-ranking.component.html 19,23 @@ -3505,6 +3660,7 @@ Blocks (1w) + Blokai (1 sav.) src/app/components/pool-ranking/pool-ranking.component.html 25 @@ -3521,6 +3677,7 @@ The number of blocks found over the past week. + Per pastarąją savaitę rastų blokų skaičius. src/app/components/pool-ranking/pool-ranking.component.html 27,31 @@ -3529,6 +3686,7 @@ Rank + Reitingas src/app/components/pool-ranking/pool-ranking.component.html 90,92 @@ -3545,6 +3703,7 @@ Empty blocks + Tušti blokai src/app/components/pool-ranking/pool-ranking.component.html 95,98 @@ -3553,6 +3712,7 @@ All miners + Visi išgavėjai src/app/components/pool-ranking/pool-ranking.component.html 113,114 @@ -3561,6 +3721,7 @@ Pools Luck (1w) + Telkinių sėkmė (1 sav.) src/app/components/pool-ranking/pool-ranking.component.html 130,132 @@ -3569,6 +3730,7 @@ Pools Count (1w) + Suskaičiuota Telkinių (1 sav.) src/app/components/pool-ranking/pool-ranking.component.html 142,144 @@ -3577,6 +3739,7 @@ Mining Pools + Gavybos Telkiniai src/app/components/pool-ranking/pool-ranking.component.ts 57 @@ -3584,6 +3747,7 @@ blocks + blokai src/app/components/pool-ranking/pool-ranking.component.ts 165,163 @@ -3595,6 +3759,7 @@ mining pool + gavybos telkinys src/app/components/pool/pool-preview.component.html 3,5 @@ -3603,6 +3768,7 @@ Tags + Žymos src/app/components/pool/pool-preview.component.html 18,19 @@ -3627,6 +3793,7 @@ Show all + Rodyti viską src/app/components/pool/pool.component.html 53,55 @@ -3647,6 +3814,7 @@ Hide + Slėpti src/app/components/pool/pool.component.html 55,58 @@ -3655,6 +3823,7 @@ Hashrate (24h) + Maišos Dažnis (24 val.) src/app/components/pool/pool.component.html 91,93 @@ -3675,6 +3844,7 @@ Estimated + Apytikris src/app/components/pool/pool.component.html 96,97 @@ -3695,6 +3865,7 @@ Reported + Pranešta src/app/components/pool/pool.component.html 97,98 @@ -3715,6 +3886,7 @@ Luck + Sėkmė src/app/components/pool/pool.component.html 98,101 @@ -3735,6 +3907,7 @@ Mined blocks + Rasta blokų src/app/components/pool/pool.component.html 141,143 @@ -3755,6 +3928,7 @@ 24h + 24 val src/app/components/pool/pool.component.html 147 @@ -3767,6 +3941,7 @@ 1w + 1 sav src/app/components/pool/pool.component.html 148 @@ -3779,6 +3954,7 @@ Coinbase tag + Monetų bazės žyma src/app/components/pool/pool.component.html 215,217 @@ -3791,6 +3967,7 @@ Broadcast Transaction + Transliavimo Operacija src/app/components/push-transaction/push-transaction.component.html 2 @@ -3808,6 +3985,7 @@ Transaction hex + Operacijos hex src/app/components/push-transaction/push-transaction.component.html 6 @@ -3820,6 +3998,7 @@ Miners Reward + Radimo atlygis src/app/components/reward-stats/reward-stats.component.html 5 @@ -3836,6 +4015,7 @@ Amount being paid to miners in the past 144 blocks + Suma, mokama išgavėjams per pastaruosius 144 blokus src/app/components/reward-stats/reward-stats.component.html 6,8 @@ -3844,6 +4024,7 @@ Reward Per Tx + Atlygis už Tx src/app/components/reward-stats/reward-stats.component.html 17 @@ -3864,6 +4045,7 @@ Average miners' reward per transaction in the past 144 blocks + Vidutinis gavybos atlygis už operaciją per pastaruosius 144 blokus src/app/components/reward-stats/reward-stats.component.html 18,20 @@ -3872,6 +4054,7 @@ sats/tx + sats/tx src/app/components/reward-stats/reward-stats.component.html 21,24 @@ -3885,6 +4068,7 @@ Average Fee + Vidutinis mokestis src/app/components/reward-stats/reward-stats.component.html 30 @@ -3897,6 +4081,7 @@ Fee paid on average for each transaction in the past 144 blocks + Vidutinis mokestis už kiekvieną operaciją per pastaruosius 144 blokus src/app/components/reward-stats/reward-stats.component.html 31,32 @@ -3905,6 +4090,7 @@ Explore the full Bitcoin ecosystem + Tyrinėkite visą Bitkoino ekosistemą src/app/components/search-form/search-form.component.html 4,6 @@ -3913,6 +4099,7 @@ Search + Ieškoti src/app/components/search-form/search-form.component.html 11,18 @@ -3921,6 +4108,7 @@ Mempool by vBytes (sat/vByte) + Mempool pagal vBitus (sat/vByte) src/app/components/statistics/statistics.component.html 7 @@ -3929,6 +4117,7 @@ TV view + TV vaizdas src/app/components/statistics/statistics.component.html 18 @@ -3941,6 +4130,7 @@ Filter + Filtruoti src/app/components/statistics/statistics.component.html 57 @@ -3949,6 +4139,7 @@ Invert + Apversti src/app/components/statistics/statistics.component.html 76 @@ -3957,6 +4148,7 @@ Transaction vBytes per second (vB/s) + Operacijos vBaitai per sekundę (vB/s) src/app/components/statistics/statistics.component.html 96 @@ -3965,6 +4157,7 @@ Just now + Dabar src/app/components/time-since/time-since.component.ts 64 @@ -3976,6 +4169,7 @@ ago + Prieš src/app/components/time-since/time-since.component.ts 74 @@ -4035,6 +4229,7 @@ After + Po src/app/components/time-span/time-span.component.ts 67 @@ -4094,6 +4289,7 @@ In ~ + Per ~ src/app/components/time-until/time-until.component.ts 66 @@ -4157,6 +4353,7 @@ This transaction has been replaced by: + Ši operacija buvo pakeista: src/app/components/transaction/transaction.component.html 5,6 @@ -4166,6 +4363,7 @@ Unconfirmed + Nepatvirtinta src/app/components/transaction/transaction.component.html 32,39 @@ -4179,6 +4377,7 @@ First seen + Pirmą kartą pamatytas src/app/components/transaction/transaction.component.html 101,102 @@ -4212,6 +4411,7 @@ ETA + Tikimasi src/app/components/transaction/transaction.component.html 108,109 @@ -4221,6 +4421,7 @@ In several hours (or more) + Po kelių valandų (ar daugiau) src/app/components/transaction/transaction.component.html 114,117 @@ -4230,6 +4431,7 @@ Descendant + Palikuonis src/app/components/transaction/transaction.component.html 161,163 @@ -4239,6 +4441,7 @@ Ancestor + Protėvis src/app/components/transaction/transaction.component.html 175,177 @@ -4248,6 +4451,7 @@ Flow + Srautas src/app/components/transaction/transaction.component.html 195,198 @@ -4261,6 +4465,7 @@ Hide diagram + Slėpti diagramą src/app/components/transaction/transaction.component.html 198,203 @@ -4269,6 +4474,7 @@ Show more + Rodyti daugiau src/app/components/transaction/transaction.component.html 219,221 @@ -4277,6 +4483,7 @@ Show less + Rodyti mažiau src/app/components/transaction/transaction.component.html 221,227 @@ -4285,6 +4492,7 @@ Show diagram + Rodyti diagramą src/app/components/transaction/transaction.component.html 241,242 @@ -4293,6 +4501,7 @@ Locktime + Užrakinimo laikas src/app/components/transaction/transaction.component.html 280,282 @@ -4301,6 +4510,7 @@ Transaction not found. + Operacija nerasta. src/app/components/transaction/transaction.component.html 443,444 @@ -4309,6 +4519,7 @@ Waiting for it to appear in the mempool... + Laukiama pasirodymo atmintinėje... src/app/components/transaction/transaction.component.html 444,449 @@ -4317,6 +4528,7 @@ Effective fee rate + Efektyvus mokesčio tarifas src/app/components/transaction/transaction.component.html 477,480 @@ -4326,6 +4538,7 @@ Coinbase + Monetų bazė src/app/components/transactions-list/transactions-list.component.html 54 @@ -4334,6 +4547,7 @@ (Newly Generated Coins) + (Naujai Sugeneruotos Monetos) src/app/components/transactions-list/transactions-list.component.html 54 @@ -4342,6 +4556,7 @@ Peg-in + Prisegimas src/app/components/transactions-list/transactions-list.component.html 56,58 @@ -4350,6 +4565,7 @@ ScriptSig (ASM) + ScriptSig (ASM) src/app/components/transactions-list/transactions-list.component.html 107,109 @@ -4359,6 +4575,7 @@ ScriptSig (HEX) + ScriptSig (HEX) src/app/components/transactions-list/transactions-list.component.html 111,114 @@ -4368,6 +4585,7 @@ Witness + Liudytojas src/app/components/transactions-list/transactions-list.component.html 116,118 @@ -4376,6 +4594,7 @@ P2SH redeem script + P2SH išpirkimo skriptas src/app/components/transactions-list/transactions-list.component.html 120,121 @@ -4384,6 +4603,7 @@ P2TR tapscript + P2TR tapskriptas src/app/components/transactions-list/transactions-list.component.html 124,126 @@ -4392,6 +4612,7 @@ P2WSH witness script + P2WSH liudininko skriptas src/app/components/transactions-list/transactions-list.component.html 126,128 @@ -4400,6 +4621,7 @@ nSequence + nSequence src/app/components/transactions-list/transactions-list.component.html 131,133 @@ -4408,6 +4630,7 @@ Previous output script + Ankstesnis išvesties skriptas src/app/components/transactions-list/transactions-list.component.html 136,137 @@ -4416,6 +4639,7 @@ Previous output type + Ankstesnis išvesties tipas src/app/components/transactions-list/transactions-list.component.html 140,141 @@ -4424,6 +4648,7 @@ Peg-out to + Atsegti į src/app/components/transactions-list/transactions-list.component.html 179,180 @@ -4432,6 +4657,7 @@ ScriptPubKey (ASM) + ScriptPubKey (ASM) src/app/components/transactions-list/transactions-list.component.html 240,242 @@ -4441,6 +4667,7 @@ ScriptPubKey (HEX) + ScriptPubKey (HEX) src/app/components/transactions-list/transactions-list.component.html 244,247 @@ -4450,6 +4677,7 @@ Show all inputs to reveal fee data + Rodyti visas įvestis, kad būtų atskleisti mokesčių duomenys src/app/components/transactions-list/transactions-list.component.html 274,277 @@ -4458,6 +4686,7 @@ other inputs + kitos įvestys src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 12 @@ -4466,6 +4695,7 @@ other outputs + kitos išvestys src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 13 @@ -4474,6 +4704,7 @@ Input + Įvestis src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 43 @@ -4482,6 +4713,7 @@ Output + Išvestis src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 44 @@ -4490,6 +4722,7 @@ This transaction saved % on fees by using native SegWit + Ši operacija sutaupė % mokesčių, naudojant vietinį SegWit src/app/components/tx-features/tx-features.component.html 2 @@ -4498,6 +4731,7 @@ SegWit + SegWit src/app/components/tx-features/tx-features.component.html 2 @@ -4515,6 +4749,7 @@ This transaction saved % on fees by using SegWit and could save % more by fully upgrading to native SegWit + Ši operacija sutaupė % mokesčių naudojant SegWit ir gali sutaupyti % daugiau visiškai naujovinus į vietinį SegWit src/app/components/tx-features/tx-features.component.html 4 @@ -4523,6 +4758,7 @@ This transaction could save % on fees by upgrading to native SegWit or % by upgrading to SegWit-P2SH + Ši operacija galėtų sutaupyti % mokesčių atnaujinus į vietinį SegWit arba % naujovinus į SegWit-P2SH src/app/components/tx-features/tx-features.component.html 6 @@ -4531,6 +4767,7 @@ This transaction uses Taproot and thereby saved at least % on fees + Ši operacija naudoja Taproot ir taip sutaupė bent % mokesčių src/app/components/tx-features/tx-features.component.html 12 @@ -4539,6 +4776,7 @@ Taproot + Taproot src/app/components/tx-features/tx-features.component.html 12 @@ -4560,6 +4798,7 @@ This transaction uses Taproot and already saved at least % on fees, but could save an additional % by fully using Taproot + Ši operacija naudoja „Taproot“ ir jau sutaupė bent % mokesčių, bet gali sutaupyti papildomai % visiškai naudojant Taproot src/app/components/tx-features/tx-features.component.html 14 @@ -4568,6 +4807,7 @@ This transaction could save % on fees by using Taproot + Ši operacija gali sutaupyti % mokesčių naudojant Taproot src/app/components/tx-features/tx-features.component.html 16 @@ -4576,6 +4816,7 @@ This transaction uses Taproot + Ši operacija naudoja Taproot src/app/components/tx-features/tx-features.component.html 18 @@ -4584,6 +4825,7 @@ This transaction supports Replace-By-Fee (RBF) allowing fee bumping + Ši operacija palaiko „Replace-By-Fee“ (RBF), leidžiančią pridėti mokesčius src/app/components/tx-features/tx-features.component.html 25 @@ -4592,6 +4834,7 @@ RBF + RBF src/app/components/tx-features/tx-features.component.html 25 @@ -4605,6 +4848,7 @@ This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method + Ši operacija nepalaiko "Replace-By-Fee" (RBF) ir negali būti paspartinta naudojant šį metodą. src/app/components/tx-features/tx-features.component.html 26 @@ -4613,6 +4857,7 @@ Optimal + Optimalus src/app/components/tx-fee-rating/tx-fee-rating.component.html 1 @@ -4622,6 +4867,7 @@ Only ~ sat/vB was needed to get into this block + Norint patekti į šį bloką, būtų užtekę ir ~ sat/vB src/app/components/tx-fee-rating/tx-fee-rating.component.html 2 @@ -4634,6 +4880,7 @@ Overpaid x + Permokėta x src/app/components/tx-fee-rating/tx-fee-rating.component.html 2 @@ -4647,6 +4894,7 @@ Transaction Fees + Operacijos mokesčiai src/app/dashboard/dashboard.component.html 6,9 @@ -4655,6 +4903,7 @@ Latest transactions + Naujausios operacijos src/app/dashboard/dashboard.component.html 120,123 @@ -4663,6 +4912,7 @@ USD + USD src/app/dashboard/dashboard.component.html 126,127 @@ -4671,6 +4921,7 @@ Minimum fee + Minimalus mokestis src/app/dashboard/dashboard.component.html 197,198 @@ -4680,6 +4931,7 @@ Purging + Valymas src/app/dashboard/dashboard.component.html 198,199 @@ -4689,6 +4941,7 @@ Memory usage + Atminties naudojimas src/app/dashboard/dashboard.component.html 210,211 @@ -4698,6 +4951,7 @@ L-BTC in circulation + L-BTC apyvartoje src/app/dashboard/dashboard.component.html 224,226 @@ -4706,6 +4960,7 @@ REST API service + REST API paslauga src/app/docs/api-docs/api-docs.component.html 34,35 @@ -4714,6 +4969,7 @@ Endpoint + Galutinis taškas src/app/docs/api-docs/api-docs.component.html 43,44 @@ -4726,6 +4982,7 @@ Description + Apibūdinimas src/app/docs/api-docs/api-docs.component.html 62,63 @@ -4737,6 +4994,7 @@ Default push: action: 'want', data: ['blocks', ...] to express what you want pushed. Available: blocks, mempool-blocks, live-2h-chart, and stats.Push transactions related to address: 'track-address': '3PbJ...bF9B' to receive all new transactions containing that address as input or output. Returns an array of transactions. address-transactions for new mempool transactions, and block-transactions for new block confirmed transactions. + Numatytasis siuntimas: veiksmas: 'want', duomenys: ['blocks', ...] , kad išreikštumėte tai, ką norite pastūmėti. Galimi: blokai , mempool blokai , realaus laiko-2val grafikas , ir statistika . Pastūmėti operacijas susietas su adresu: 'track-address': '3PbJ...bF9B' priimti visas naujas operacijas susietas su adresu kaip įvestis ar išvestis. Pateikiama kaip operacijų rinkinys. adreso-operacijosnaujoms mempool operacijoms, ir bloko operacijosnaujoms bloke patvirtintoms operacijoms. src/app/docs/api-docs/api-docs.component.html 102,103 @@ -4745,6 +5003,7 @@ Code Example + Kodo Pavyzdys src/app/docs/code-template/code-template.component.html 6,7 @@ -4765,6 +5024,7 @@ Install Package + Įdiegti Paketą src/app/docs/code-template/code-template.component.html 23,24 @@ -4773,6 +5033,7 @@ Response + Atsakymas src/app/docs/code-template/code-template.component.html 43,44 @@ -4781,6 +5042,7 @@ FAQ + DUK src/app/docs/docs/docs.component.ts 33 @@ -4788,6 +5050,7 @@ API + API src/app/docs/docs/docs.component.ts 36 @@ -4799,6 +5062,7 @@ Base fee + Bazės mokestis src/app/lightning/channel/channel-box/channel-box.component.html 30 @@ -4811,6 +5075,7 @@ mSats + mSats src/app/lightning/channel/channel-box/channel-box.component.html 36 @@ -4827,6 +5092,7 @@ This channel supports zero base fee routing + Šis kanalas palaiko nulinio bazinio mokesčio maršrutą src/app/lightning/channel/channel-box/channel-box.component.html 45 @@ -4835,6 +5101,7 @@ Zero base fee + Nulinis bazinis mokestis src/app/lightning/channel/channel-box/channel-box.component.html 46 @@ -4843,6 +5110,7 @@ This channel does not support zero base fee routing + Šis kanalas nepalaiko nulinio bazinio mokesčio src/app/lightning/channel/channel-box/channel-box.component.html 51 @@ -4851,6 +5119,7 @@ Non-zero base fee + Nenulinis bazinis mokestis src/app/lightning/channel/channel-box/channel-box.component.html 52 @@ -4859,6 +5128,7 @@ Min HTLC + Min. HTLC src/app/lightning/channel/channel-box/channel-box.component.html 58 @@ -4867,6 +5137,7 @@ Max HTLC + Maks HTLC src/app/lightning/channel/channel-box/channel-box.component.html 64 @@ -4875,6 +5146,7 @@ Timelock delta + Laiko užrakto delta src/app/lightning/channel/channel-box/channel-box.component.html 70 @@ -4883,6 +5155,7 @@ channels + kanalai src/app/lightning/channel/channel-box/channel-box.component.html 80 @@ -4895,6 +5168,7 @@ lightning channel + žaibatinklio kanalas src/app/lightning/channel/channel-preview.component.html 3,5 @@ -4903,6 +5177,7 @@ Inactive + Neaktyvus src/app/lightning/channel/channel-preview.component.html 10,11 @@ -4919,6 +5194,7 @@ Active + Aktyvus src/app/lightning/channel/channel-preview.component.html 11,12 @@ -4935,6 +5211,7 @@ Closed + Uždarytas src/app/lightning/channel/channel-preview.component.html 12,14 @@ -4955,6 +5232,7 @@ Created + Sukurta src/app/lightning/channel/channel-preview.component.html 23,26 @@ -4967,6 +5245,7 @@ Capacity + Talpa src/app/lightning/channel/channel-preview.component.html 27,28 @@ -5019,6 +5298,7 @@ ppm + ppm src/app/lightning/channel/channel-preview.component.html 34,35 @@ -5039,6 +5319,7 @@ Lightning channel + Žaibatinklio kanalas src/app/lightning/channel/channel.component.html 2,5 @@ -5051,6 +5332,7 @@ Last update + Atnaujinta src/app/lightning/channel/channel.component.html 33,34 @@ -5083,6 +5365,7 @@ Closing date + Uždarymo data src/app/lightning/channel/channel.component.html 37,38 @@ -5095,6 +5378,7 @@ Opening transaction + Atidarymo operacija src/app/lightning/channel/channel.component.html 73,74 @@ -5103,6 +5387,7 @@ Closing transaction + Uždarymo operacija src/app/lightning/channel/channel.component.html 82,84 @@ -5111,6 +5396,7 @@ Channel: + Kanalas: src/app/lightning/channel/channel.component.ts 37 @@ -5118,6 +5404,7 @@ Open + Atidarytas src/app/lightning/channels-list/channels-list.component.html 5,7 @@ -5126,6 +5413,7 @@ No channels to display + Nėra kanalų, kuriuos būtų galima rodyti src/app/lightning/channels-list/channels-list.component.html 29,35 @@ -5134,6 +5422,7 @@ Alias + Pseudonimas src/app/lightning/channels-list/channels-list.component.html 35,37 @@ -5170,6 +5459,7 @@ Status + Būsena src/app/lightning/channels-list/channels-list.component.html 37,38 @@ -5178,6 +5468,7 @@ Channel ID + Kanalo ID src/app/lightning/channels-list/channels-list.component.html 41,45 @@ -5186,6 +5477,7 @@ sats + sats src/app/lightning/channels-list/channels-list.component.html 61,65 @@ -5238,6 +5530,7 @@ Avg Capacity + Vid. Talpa src/app/lightning/channels-statistics/channels-statistics.component.html 13,15 @@ -5250,6 +5543,7 @@ Avg Fee Rate + Vid. Mokesčio Tarifas src/app/lightning/channels-statistics/channels-statistics.component.html 26,28 @@ -5262,6 +5556,7 @@ The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Vidutinis maršruto mazgų imamas mokesčio tarifas, neatsižvelgiant į mokesčių tarifus > 0,5% arba 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 28,30 @@ -5270,6 +5565,7 @@ Avg Base Fee + Vidutinis Bazinis Mokestis src/app/lightning/channels-statistics/channels-statistics.component.html 41,43 @@ -5282,6 +5578,7 @@ The average base fee charged by routing nodes, ignoring base fees > 5000ppm + Vidutinis bazinis mokestis, kurį ima maršruto parinkimo mazgai, neatsižvelgiant į bazinius mokesčius > 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 43,45 @@ -5290,6 +5587,7 @@ Med Capacity + Talpumo Mediana src/app/lightning/channels-statistics/channels-statistics.component.html 59,61 @@ -5298,6 +5596,7 @@ Med Fee Rate + Mokesčio Mediana src/app/lightning/channels-statistics/channels-statistics.component.html 72,74 @@ -5306,6 +5605,7 @@ The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Maršrutizavimo mazgų imama mokesčio tarifo mediana, neatsižvelgiant į mokesčių tarifus > 0,5 % arba 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 74,76 @@ -5314,6 +5614,7 @@ Med Base Fee + Bazinio Mokesčio Mediana src/app/lightning/channels-statistics/channels-statistics.component.html 87,89 @@ -5322,6 +5623,7 @@ The median base fee charged by routing nodes, ignoring base fees > 5000ppm + Bazinio mokesčio mediana, kurią ima maršruto mazgai, neatsižvelgiant į bazinius mokesčius > 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 89,91 @@ -5330,6 +5632,7 @@ Lightning node group + Žaibatinklio mazgų grupė src/app/lightning/group/group-preview.component.html 3,5 @@ -5342,6 +5645,7 @@ Nodes + Mazgai src/app/lightning/group/group-preview.component.html 25,29 @@ -5382,6 +5686,7 @@ Liquidity + Likvidumas src/app/lightning/group/group-preview.component.html 29,31 @@ -5418,6 +5723,7 @@ Channels + Kanalai src/app/lightning/group/group-preview.component.html 40,43 @@ -5478,6 +5784,7 @@ Average size + Vidutinis dydis src/app/lightning/group/group-preview.component.html 44,46 @@ -5490,6 +5797,7 @@ Location + Lokacija src/app/lightning/group/group.component.html 74,77 @@ -5530,6 +5838,7 @@ Network Statistics + Tinklo Statistika src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 10 @@ -5538,6 +5847,7 @@ Channels Statistics + Kanalų Statistika src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 24 @@ -5546,6 +5856,7 @@ Lightning Network History + Žaibo Tinklo Istorija src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 49 @@ -5554,6 +5865,7 @@ Liquidity Ranking + Likvidumo Reitingas src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 62 @@ -5570,6 +5882,7 @@ Connectivity Ranking + Ryšio Reitingas src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 76 @@ -5582,6 +5895,7 @@ Percentage change past week + Praėjusios savaitės procentinis pokytis src/app/lightning/node-statistics/node-statistics.component.html 5,7 @@ -5598,6 +5912,7 @@ Lightning node + Žaibatinklio mazgas src/app/lightning/node/node-preview.component.html 3,5 @@ -5614,6 +5929,7 @@ Active capacity + Aktyvi talpa src/app/lightning/node/node-preview.component.html 20,22 @@ -5626,6 +5942,7 @@ Active channels + Aktyvūs kanalai src/app/lightning/node/node-preview.component.html 26,30 @@ -5638,6 +5955,7 @@ Country + Šalis src/app/lightning/node/node-preview.component.html 44,47 @@ -5646,6 +5964,7 @@ No node found for public key "" + Viešajam raktui mazgas nerastas &quot; &quot; src/app/lightning/node/node.component.html 17,19 @@ -5654,6 +5973,7 @@ Average channel size + Vidutinis kanalo dydis src/app/lightning/node/node.component.html 40,43 @@ -5662,6 +5982,7 @@ Unknown + Nežinoma src/app/lightning/node/node.component.html 52,56 @@ -5682,6 +6003,7 @@ Color + Spalva src/app/lightning/node/node.component.html 75,77 @@ -5690,6 +6012,7 @@ ISP + IPT src/app/lightning/node/node.component.html 82,83 @@ -5702,6 +6025,7 @@ Exclusively on Tor + Tik Tor src/app/lightning/node/node.component.html 88,90 @@ -5710,6 +6034,7 @@ Open channels + Atviri kanalai src/app/lightning/node/node.component.html 145,148 @@ -5718,6 +6043,7 @@ Closed channels + Uždaryti kanalai src/app/lightning/node/node.component.html 149,152 @@ -5726,6 +6052,7 @@ Node: + Mazgas: src/app/lightning/node/node.component.ts 42 @@ -5733,6 +6060,7 @@ (Tor nodes excluded) + (Be Tor mazgų) src/app/lightning/nodes-channels-map/nodes-channels-map.component.html 8,11 @@ -5753,6 +6081,7 @@ Lightning Nodes Channels World Map + Žaibatinklio Mazgų Kanalų Pasaulio Žemėlapis src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 69 @@ -5760,6 +6089,7 @@ No geolocation data available + Geografinės vietos duomenų nėra src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 218,213 @@ -5767,6 +6097,7 @@ Active channels map + Aktyvių kanalų žemėlapis src/app/lightning/nodes-channels/node-channels.component.html 2,3 @@ -5775,6 +6106,7 @@ Indexing in progess + Vyksta indeksavimas src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 121,116 @@ -5786,6 +6118,7 @@ Reachable on Clearnet Only + Pasiekiama tik per Clearnet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 164,161 @@ -5797,6 +6130,7 @@ Reachable on Clearnet and Darknet + Galima Pasiekti per Clearnet ir Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 185,182 @@ -5808,6 +6142,7 @@ Reachable on Darknet Only + Pasiekiamas tik per Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 206,203 @@ -5819,6 +6154,7 @@ Share + Dalis src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html 29,31 @@ -5831,6 +6167,7 @@ nodes + mazgai src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 103,102 @@ -5846,6 +6183,7 @@ BTC capacity + BTC talpumas src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 104,102 @@ -5853,6 +6191,7 @@ Lightning nodes in + Žaibatinklio mazgai src/app/lightning/nodes-per-country/nodes-per-country.component.html 3,4 @@ -5861,6 +6200,7 @@ ISP Count + IPT Skaičius src/app/lightning/nodes-per-country/nodes-per-country.component.html 34,38 @@ -5869,6 +6209,7 @@ Top ISP + Populiariausias IPT src/app/lightning/nodes-per-country/nodes-per-country.component.html 38,40 @@ -5877,6 +6218,7 @@ Lightning nodes in + Žaibatinklio mazgai src/app/lightning/nodes-per-country/nodes-per-country.component.ts 35 @@ -5884,6 +6226,7 @@ Clearnet Capacity + Clearnet Talpa src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 6,8 @@ -5896,6 +6239,7 @@ How much liquidity is running on nodes advertising at least one clearnet IP address + Kiek likvidumo turi mazgai, paskelbę bent vieną „clearnet“ IP adresą src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 8,9 @@ -5904,6 +6248,7 @@ Unknown Capacity + Nežinoma Talpa src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 13,15 @@ -5916,6 +6261,7 @@ How much liquidity is running on nodes which ISP was not identifiable + Kiek likvidumo turi mazgai, kurių IPT neatpažintas src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 15,16 @@ -5924,6 +6270,7 @@ Tor Capacity + Tor Talpa src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 20,22 @@ -5936,6 +6283,7 @@ How much liquidity is running on nodes advertising only Tor addresses + Kiek likvidumo turi mazgai, paskelbę tik „Tor“ adresus src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 22,23 @@ -5944,6 +6292,7 @@ Top 100 ISPs hosting LN nodes + 100 geriausių IPT, kuriuose yra ŽT mazgai src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 31,33 @@ -5952,6 +6301,7 @@ BTC + BTC src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 158,156 @@ -5963,6 +6313,7 @@ Lightning ISP + Žaibatinklio IPT src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 3,5 @@ -5971,6 +6322,7 @@ Top country + Geriausia šalis src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 39,41 @@ -5983,6 +6335,7 @@ Top node + Geriausias mazgas src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 45,48 @@ -5991,6 +6344,7 @@ Lightning nodes on ISP: [AS] + Žaibatinklio mazgai ant IPT: [AS ] src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 44 @@ -6002,6 +6356,7 @@ Lightning nodes on ISP: + Žaibatinklio mazgai ant IPT: src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 2,4 @@ -6010,6 +6365,7 @@ ASN + ASN src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 11,14 @@ -6018,6 +6374,7 @@ Top 100 oldest lightning nodes + 100 seniausių žaibatinklio mazgų src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html 3,7 @@ -6026,6 +6383,7 @@ Oldest lightning nodes + Seniausi žaibatinklio mazgai src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 27 @@ -6033,6 +6391,7 @@ Top 100 nodes liquidity ranking + 100 geriausių mazgų likvidumo reitingas src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html 3,7 @@ -6041,6 +6400,7 @@ Top 100 nodes connectivity ranking + 100 geriausių mazgų ryšio reitingas src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html 3,7 @@ -6049,6 +6409,7 @@ Oldest nodes + Seniausi mazgai src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html 36 @@ -6057,6 +6418,7 @@ Top lightning nodes + Geriausi žaibatinklio mazgai src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 22 @@ -6064,6 +6426,7 @@ Indexing in progress + Vyksta indeksavimas src/app/lightning/statistics-chart/lightning-statistics-chart.component.html 52,55 @@ -6072,6 +6435,7 @@ year + metai src/app/shared/i18n/dates.ts 3 @@ -6079,6 +6443,7 @@ years + metai src/app/shared/i18n/dates.ts 4 @@ -6086,6 +6451,7 @@ month + mėn src/app/shared/i18n/dates.ts 5 @@ -6093,6 +6459,7 @@ months + mėn src/app/shared/i18n/dates.ts 6 @@ -6100,6 +6467,7 @@ week + sav src/app/shared/i18n/dates.ts 7 @@ -6107,6 +6475,7 @@ weeks + sav src/app/shared/i18n/dates.ts 8 @@ -6114,6 +6483,7 @@ day + diena src/app/shared/i18n/dates.ts 9 @@ -6121,6 +6491,7 @@ days + dienos src/app/shared/i18n/dates.ts 10 @@ -6128,6 +6499,7 @@ hour + val src/app/shared/i18n/dates.ts 11 @@ -6135,6 +6507,7 @@ hours + val src/app/shared/i18n/dates.ts 12 @@ -6142,6 +6515,7 @@ minute + min src/app/shared/i18n/dates.ts 13 @@ -6149,6 +6523,7 @@ minutes + min src/app/shared/i18n/dates.ts 14 @@ -6156,6 +6531,7 @@ second + sek src/app/shared/i18n/dates.ts 15 @@ -6163,6 +6539,7 @@ seconds + sek src/app/shared/i18n/dates.ts 16 @@ -6170,6 +6547,7 @@ Transaction fee + Operacijos mokestis src/app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts 11 diff --git a/frontend/src/locale/messages.ru.xlf b/frontend/src/locale/messages.ru.xlf index b65e897e2..2a1561494 100644 --- a/frontend/src/locale/messages.ru.xlf +++ b/frontend/src/locale/messages.ru.xlf @@ -1471,6 +1471,7 @@ Community Integrations + Интеграции c сообществом src/app/components/about/about.component.html 191,193 @@ -1544,6 +1545,7 @@ Multisig of + Мультиподпись из src/app/components/address-labels/address-labels.component.ts 105 @@ -2027,6 +2029,7 @@ Block + Блок src/app/components/block-audit/block-audit.component.html 7,9 @@ -2121,6 +2124,7 @@ Match rate + Коэффициент соответствия src/app/components/block-audit/block-audit.component.html 64,67 @@ -2129,6 +2133,7 @@ Missing txs + Отсутствующие транзакции src/app/components/block-audit/block-audit.component.html 68,71 @@ -2137,6 +2142,7 @@ Added txs + Добавленные транзакции src/app/components/block-audit/block-audit.component.html 72,75 @@ -2145,6 +2151,7 @@ Missing + Отсутствующие src/app/components/block-audit/block-audit.component.html 84,85 @@ -2153,6 +2160,7 @@ Added + Добавленные src/app/components/block-audit/block-audit.component.html 86,92 @@ -2470,6 +2478,7 @@ No data to display yet. Try again later. + Пока нет данных для отображения. Попробуйте позже. src/app/components/block-prediction-graph/block-prediction-graph.component.ts 108,103 @@ -2523,6 +2532,7 @@ Block + Блок src/app/components/block/block-preview.component.html 3,7 @@ -2535,6 +2545,7 @@ + src/app/components/block/block-preview.component.html 11,12 @@ -3250,6 +3261,7 @@ Hashrate & Difficulty + Хэшрейт и сложность src/app/components/graphs/graphs.component.html 15,16 @@ -3258,6 +3270,7 @@ Lightning + Лайтнинг src/app/components/graphs/graphs.component.html 31 @@ -3266,6 +3279,7 @@ Lightning Nodes Per Network + Узлы Лайтнинг на сеть src/app/components/graphs/graphs.component.html 34 @@ -3286,6 +3300,7 @@ Lightning Network Capacity + Объем сети Лайтнинг src/app/components/graphs/graphs.component.html 36 @@ -3306,6 +3321,7 @@ Lightning Nodes Per ISP + Узлов Лайтнинг на провайдера src/app/components/graphs/graphs.component.html 38 @@ -3318,6 +3334,7 @@ Lightning Nodes Per Country + Узлов Лайтнинг на страну src/app/components/graphs/graphs.component.html 40 @@ -3334,6 +3351,7 @@ Lightning Nodes World Map + Мировая карта узлов Лайтнинг src/app/components/graphs/graphs.component.html 42 @@ -3350,6 +3368,7 @@ Lightning Nodes Channels World Map + Мировая карта лайтнинг-каналов src/app/components/graphs/graphs.component.html 44 @@ -3470,6 +3489,7 @@ Lightning Explorer + Лайтнинг-обозреватель src/app/components/master-page/master-page.component.html 44,45 @@ -3482,6 +3502,7 @@ beta + бета src/app/components/master-page/master-page.component.html 45,48 @@ -3737,6 +3758,7 @@ mining pool + майнинг-пул src/app/components/pool/pool-preview.component.html 3,5 @@ -4067,6 +4089,7 @@ Explore the full Bitcoin ecosystem + Исследуйте всю экосистему Биткоина src/app/components/search-form/search-form.component.html 4,6 @@ -4440,6 +4463,7 @@ Hide diagram + Скрыть диаграмму src/app/components/transaction/transaction.component.html 198,203 @@ -4448,6 +4472,7 @@ Show more + Показать больше src/app/components/transaction/transaction.component.html 219,221 @@ -4456,6 +4481,7 @@ Show less + Показывай меньше src/app/components/transaction/transaction.component.html 221,227 @@ -4464,6 +4490,7 @@ Show diagram + Показать схему src/app/components/transaction/transaction.component.html 241,242 @@ -4657,6 +4684,7 @@ other inputs + другие входы src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 12 @@ -4665,6 +4693,7 @@ other outputs + другие выходы src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 13 @@ -4673,6 +4702,7 @@ Input + Вход src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 43 @@ -4681,6 +4711,7 @@ Output + Выход src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 44 @@ -4689,6 +4720,7 @@ This transaction saved % on fees by using native SegWit + Эта транзакция сэкономила % на комиссиях за счет использования нативного SegWit. src/app/components/tx-features/tx-features.component.html 2 @@ -4715,6 +4747,7 @@ This transaction saved % on fees by using SegWit and could save % more by fully upgrading to native SegWit + Эта транзакция сэкономила % на комиссиях за счет использования SegWit и может сэкономить еще % за счет полного перехода на нативный SegWit. src/app/components/tx-features/tx-features.component.html 4 @@ -4723,6 +4756,7 @@ This transaction could save % on fees by upgrading to native SegWit or % by upgrading to SegWit-P2SH + Эта транзакция могла сэкономить % на комиссиях за счет перехода на нативный SegWit или % за счет обновления до SegWit-P2SH. src/app/components/tx-features/tx-features.component.html 6 @@ -4731,6 +4765,7 @@ This transaction uses Taproot and thereby saved at least % on fees + Эта транзакция использует Taproot и, таким образом, сэкономила как минимум % на комиссиях. src/app/components/tx-features/tx-features.component.html 12 @@ -4739,6 +4774,7 @@ Taproot + Taproot src/app/components/tx-features/tx-features.component.html 12 @@ -4760,6 +4796,7 @@ This transaction uses Taproot and already saved at least % on fees, but could save an additional % by fully using Taproot + Эта транзакция использует Taproot и уже сэкономила как минимум % на комиссиях, но может сэкономить дополнительно % за счет использования Taproot в полной мере. src/app/components/tx-features/tx-features.component.html 14 @@ -4768,6 +4805,7 @@ This transaction could save % on fees by using Taproot + Эта транзакция может сэкономить % на комиссиях с помощью Taproot. src/app/components/tx-features/tx-features.component.html 16 @@ -4785,6 +4823,7 @@ This transaction supports Replace-By-Fee (RBF) allowing fee bumping + Эта транзакция поддерживает функцию Replace-By-Fee (RBF), что позволяет повышать комиссию. src/app/components/tx-features/tx-features.component.html 25 @@ -5021,6 +5060,7 @@ Base fee + Базовая комиссия src/app/lightning/channel/channel-box/channel-box.component.html 30 @@ -5033,6 +5073,7 @@ mSats + мСат src/app/lightning/channel/channel-box/channel-box.component.html 36 @@ -5049,6 +5090,7 @@ This channel supports zero base fee routing + Этот канал поддерживает маршрутизацию с нулевой базовой комиссией. src/app/lightning/channel/channel-box/channel-box.component.html 45 @@ -5057,6 +5099,7 @@ Zero base fee + Нулевая базовая комиссия src/app/lightning/channel/channel-box/channel-box.component.html 46 @@ -5065,6 +5108,7 @@ This channel does not support zero base fee routing + Этот канал не поддерживает маршрутизацию с нулевой базовой комиссией. src/app/lightning/channel/channel-box/channel-box.component.html 51 @@ -5073,6 +5117,7 @@ Non-zero base fee + Ненулевая базовая комиссия src/app/lightning/channel/channel-box/channel-box.component.html 52 @@ -5081,6 +5126,7 @@ Min HTLC + Мин. HTLC src/app/lightning/channel/channel-box/channel-box.component.html 58 @@ -5089,6 +5135,7 @@ Max HTLC + Макс. HTLC src/app/lightning/channel/channel-box/channel-box.component.html 64 @@ -5105,6 +5152,7 @@ channels + каналов src/app/lightning/channel/channel-box/channel-box.component.html 80 @@ -5117,6 +5165,7 @@ lightning channel + лайтнинг-канал src/app/lightning/channel/channel-preview.component.html 3,5 @@ -5125,6 +5174,7 @@ Inactive + Неактивный src/app/lightning/channel/channel-preview.component.html 10,11 @@ -5141,6 +5191,7 @@ Active + Активный src/app/lightning/channel/channel-preview.component.html 11,12 @@ -5157,6 +5208,7 @@ Closed + Закрыт src/app/lightning/channel/channel-preview.component.html 12,14 @@ -5177,6 +5229,7 @@ Created + Создан src/app/lightning/channel/channel-preview.component.html 23,26 @@ -5189,6 +5242,7 @@ Capacity + Объем src/app/lightning/channel/channel-preview.component.html 27,28 @@ -5261,6 +5315,7 @@ Lightning channel + Лайтнинг-канал src/app/lightning/channel/channel.component.html 2,5 @@ -5273,6 +5328,7 @@ Last update + Последнее обновление src/app/lightning/channel/channel.component.html 33,34 @@ -5305,6 +5361,7 @@ Closing date + Дата закрытия src/app/lightning/channel/channel.component.html 37,38 @@ -5317,6 +5374,7 @@ Opening transaction + Транзакция открытия src/app/lightning/channel/channel.component.html 73,74 @@ -5325,6 +5383,7 @@ Closing transaction + Транзакция закрытия src/app/lightning/channel/channel.component.html 82,84 @@ -5333,6 +5392,7 @@ Channel: + Канал: src/app/lightning/channel/channel.component.ts 37 @@ -5348,6 +5408,7 @@ No channels to display + Нет каналов для отображения src/app/lightning/channels-list/channels-list.component.html 29,35 @@ -5356,6 +5417,7 @@ Alias + Псевдоним src/app/lightning/channels-list/channels-list.component.html 35,37 @@ -5392,6 +5454,7 @@ Status + Статус src/app/lightning/channels-list/channels-list.component.html 37,38 @@ -5400,6 +5463,7 @@ Channel ID + ID канала src/app/lightning/channels-list/channels-list.component.html 41,45 @@ -5408,6 +5472,7 @@ sats + сат src/app/lightning/channels-list/channels-list.component.html 61,65 @@ -5460,6 +5525,7 @@ Avg Capacity + Средняя емкость src/app/lightning/channels-statistics/channels-statistics.component.html 13,15 @@ -5472,6 +5538,7 @@ Avg Fee Rate + Средняя комиссия src/app/lightning/channels-statistics/channels-statistics.component.html 26,28 @@ -5484,6 +5551,7 @@ The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Средняя комиссия, взимаемая узлами маршрутизации, без учета ставок комиссии > 0,5% или 5000 ppm. src/app/lightning/channels-statistics/channels-statistics.component.html 28,30 @@ -5492,6 +5560,7 @@ Avg Base Fee + Средняя базовая комиссия src/app/lightning/channels-statistics/channels-statistics.component.html 41,43 @@ -5504,6 +5573,7 @@ The average base fee charged by routing nodes, ignoring base fees > 5000ppm + Средняя базовая комиссия, взимаемая узлами маршрутизации, без учета базовых комиссий > 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 43,45 @@ -5512,6 +5582,7 @@ Med Capacity + Медианныйобъем src/app/lightning/channels-statistics/channels-statistics.component.html 59,61 @@ -5520,6 +5591,7 @@ Med Fee Rate + Медианная комиссия src/app/lightning/channels-statistics/channels-statistics.component.html 72,74 @@ -5528,6 +5600,7 @@ The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Медианная комиссия, взимаемая узлами маршрутизации, без учета ставок комиссии > 0,5% или 5000 ppm. src/app/lightning/channels-statistics/channels-statistics.component.html 74,76 @@ -5536,6 +5609,7 @@ Med Base Fee + Медианная базовая комиссия src/app/lightning/channels-statistics/channels-statistics.component.html 87,89 @@ -5544,6 +5618,7 @@ The median base fee charged by routing nodes, ignoring base fees > 5000ppm + Медианная базовая комиссия, взимаемая узлами маршрутизации, без учета базовых комиссий > 5000 ppm src/app/lightning/channels-statistics/channels-statistics.component.html 89,91 @@ -5552,6 +5627,7 @@ Lightning node group + Группа лайтнинг-узлов src/app/lightning/group/group-preview.component.html 3,5 @@ -5564,6 +5640,7 @@ Nodes + Узлы src/app/lightning/group/group-preview.component.html 25,29 @@ -5604,6 +5681,7 @@ Liquidity + Ликвидность src/app/lightning/group/group-preview.component.html 29,31 @@ -5640,6 +5718,7 @@ Channels + Каналы src/app/lightning/group/group-preview.component.html 40,43 @@ -5700,6 +5779,7 @@ Average size + Средний размер src/app/lightning/group/group-preview.component.html 44,46 @@ -5712,6 +5792,7 @@ Location + Расположение src/app/lightning/group/group.component.html 74,77 @@ -5752,6 +5833,7 @@ Network Statistics + Статистика сети src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 10 @@ -5760,6 +5842,7 @@ Channels Statistics + Статистика каналов src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 24 @@ -5768,6 +5851,7 @@ Lightning Network History + История сети Лайтнинг src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 49 @@ -5776,6 +5860,7 @@ Liquidity Ranking + Рейтинг ликвидности src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 62 @@ -5792,6 +5877,7 @@ Connectivity Ranking + Рейтинг соединения src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 76 @@ -5804,6 +5890,7 @@ Percentage change past week + Процентное изменение за последнюю неделю src/app/lightning/node-statistics/node-statistics.component.html 5,7 @@ -5820,6 +5907,7 @@ Lightning node + Лайтнинг-узел src/app/lightning/node/node-preview.component.html 3,5 @@ -5836,6 +5924,7 @@ Active capacity + Активная емкость src/app/lightning/node/node-preview.component.html 20,22 @@ -5848,6 +5937,7 @@ Active channels + Активные каналы src/app/lightning/node/node-preview.component.html 26,30 @@ -5860,6 +5950,7 @@ Country + Страна src/app/lightning/node/node-preview.component.html 44,47 @@ -5868,6 +5959,7 @@ No node found for public key "" + Не найден узел для открытого ключа &quot; &quot; src/app/lightning/node/node.component.html 17,19 @@ -5876,6 +5968,7 @@ Average channel size + Средний размер канала src/app/lightning/node/node.component.html 40,43 @@ -5884,6 +5977,7 @@ Unknown + Неизвестно src/app/lightning/node/node.component.html 52,56 @@ -5904,6 +5998,7 @@ Color + Цвет src/app/lightning/node/node.component.html 75,77 @@ -5912,6 +6007,7 @@ ISP + Интернет-провайдер src/app/lightning/node/node.component.html 82,83 @@ -5924,6 +6020,7 @@ Exclusively on Tor + Исключительно Tor src/app/lightning/node/node.component.html 88,90 @@ -5932,6 +6029,7 @@ Open channels + Открытые каналы src/app/lightning/node/node.component.html 145,148 @@ -5940,6 +6038,7 @@ Closed channels + Закрытые каналы src/app/lightning/node/node.component.html 149,152 @@ -5948,6 +6047,7 @@ Node: + Узел: src/app/lightning/node/node.component.ts 42 @@ -5955,6 +6055,7 @@ (Tor nodes excluded) + (узлы Tor исключены) src/app/lightning/nodes-channels-map/nodes-channels-map.component.html 8,11 @@ -5975,6 +6076,7 @@ Lightning Nodes Channels World Map + Мировая карта каналов лайтнинг-узлов src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 69 @@ -5982,6 +6084,7 @@ No geolocation data available + Данные геолокации недоступны src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 218,213 @@ -5989,6 +6092,7 @@ Active channels map + Карта активных каналов src/app/lightning/nodes-channels/node-channels.component.html 2,3 @@ -5997,6 +6101,7 @@ Indexing in progess + Индексирование в процессе src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 121,116 @@ -6008,6 +6113,7 @@ Reachable on Clearnet Only + Доступно только в Clearnet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 164,161 @@ -6019,6 +6125,7 @@ Reachable on Clearnet and Darknet + Доступно в Clearnet и Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 185,182 @@ -6030,6 +6137,7 @@ Reachable on Darknet Only + Доступно только в Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 206,203 @@ -6041,6 +6149,7 @@ Share + Поделиться src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html 29,31 @@ -6053,6 +6162,7 @@ nodes + узлы src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 103,102 @@ -6068,6 +6178,7 @@ BTC capacity + Емкость BTC src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 104,102 @@ -6075,6 +6186,7 @@ Lightning nodes in + лайтнинг-злы в src/app/lightning/nodes-per-country/nodes-per-country.component.html 3,4 @@ -6083,6 +6195,7 @@ ISP Count + Количество интернет-провайдеров src/app/lightning/nodes-per-country/nodes-per-country.component.html 34,38 @@ -6091,6 +6204,7 @@ Top ISP + Топ интернет-провайдеры src/app/lightning/nodes-per-country/nodes-per-country.component.html 38,40 @@ -6099,6 +6213,7 @@ Lightning nodes in + лайтнинг-узлы в src/app/lightning/nodes-per-country/nodes-per-country.component.ts 35 @@ -6106,6 +6221,7 @@ Clearnet Capacity + Емкость Clearnet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 6,8 @@ -6118,6 +6234,7 @@ How much liquidity is running on nodes advertising at least one clearnet IP address + Сколько ликвидности в узлах, сообщающих хотя бы один IP-адрес на Clearnet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 8,9 @@ -6126,6 +6243,7 @@ Unknown Capacity + Емкость неизвестна src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 13,15 @@ -6138,6 +6256,7 @@ How much liquidity is running on nodes which ISP was not identifiable + Сколько ликвидности в узлах, интернет-провайдер которых не удалось идентифицировать src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 15,16 @@ -6146,6 +6265,7 @@ Tor Capacity + Емкость Tor src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 20,22 @@ -6158,6 +6278,7 @@ How much liquidity is running on nodes advertising only Tor addresses + Сколько ликвидности в узлах, сообщающих только адреса Tor src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 22,23 @@ -6166,6 +6287,7 @@ Top 100 ISPs hosting LN nodes + Топ-100 интернет-провайдеров, размещающих узлы LN src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 31,33 @@ -6174,6 +6296,7 @@ BTC + BTC src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 158,156 @@ -6185,6 +6308,7 @@ Lightning ISP + Лайтнинг интернет-провайдер src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 3,5 @@ -6193,6 +6317,7 @@ Top country + Топ страна src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 39,41 @@ -6205,6 +6330,7 @@ Top node + Топ узел src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 45,48 @@ -6213,6 +6339,7 @@ Lightning nodes on ISP: [AS] + Лайтнинг-узлы у провайдера: [AS ] src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 44 @@ -6224,6 +6351,7 @@ Lightning nodes on ISP: + Лайтнинг-узлы у провайдера: src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 2,4 @@ -6232,6 +6360,7 @@ ASN + ASN src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 11,14 @@ -6240,6 +6369,7 @@ Top 100 oldest lightning nodes + Топ-100 самых старых узлов Lightning src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html 3,7 @@ -6248,6 +6378,7 @@ Oldest lightning nodes + Самые старые лайтнинг-узлы src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 27 @@ -6255,6 +6386,7 @@ Top 100 nodes liquidity ranking + Топ-100 узлов по ликвидности src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html 3,7 @@ -6263,6 +6395,7 @@ Top 100 nodes connectivity ranking + Топ-100 узлов по подключению src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html 3,7 @@ -6271,6 +6404,7 @@ Oldest nodes + Самые старые узлы src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html 36 @@ -6279,6 +6413,7 @@ Top lightning nodes + Топ лайтнинг-узлы src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 22 @@ -6286,6 +6421,7 @@ Indexing in progress + Выполняется индексирование src/app/lightning/statistics-chart/lightning-statistics-chart.component.html 52,55 diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index d3d16d12e..b0cdb5ef6 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -115,10 +115,38 @@ body { } .form-control { - color: #495057; + color: #fff; + background-color: #2d3348; + border: 1px solid rgba(17, 19, 31, 0.2); } + .form-control:focus { - color: #000; + color: #fff; + background-color: #2d3348; +} + +.btn-purple { + background-color: #653b9c; + border-color: #653b9c; +} + +.btn-purple:not(:disabled):not(.disabled):active, .btn-purple:not(:disabled):not(.disabled).active, .show > .btn-purple.dropdown-toggle { + color: #fff; + background-color: #4d2d77; + border-color: #472a6e; +} + +.btn-purple:focus, .btn-purple.focus { + color: #fff; + background-color: #533180; + border-color: #4d2d77; + box-shadow: 0 0 0 0.2rem rgb(124 88 171 / 50%); +} + +.btn-purple:hover { + color: #fff; + background-color: #533180; + border-color: #4d2d77; } .form-control.form-control-secondary { diff --git a/frontend/sync-assets.js b/frontend/sync-assets.js index 9c447bf7d..879a7fba4 100644 --- a/frontend/sync-assets.js +++ b/frontend/sync-assets.js @@ -54,9 +54,13 @@ function downloadMiningPoolLogos() { response.on('end', () => { let response_body = Buffer.concat(chunks_of_data); - const poolLogos = JSON.parse(response_body.toString()); - for (const poolLogo of poolLogos) { - download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url); + try { + const poolLogos = JSON.parse(response_body.toString()); + for (const poolLogo of poolLogos) { + download(`${PATH}/mining-pools/${poolLogo.name}`, poolLogo.download_url); + } + } catch (e) { + console.error(`Unable to download mining pool logos. Trying again at next restart. Reason: ${e instanceof Error ? e.message : e}`); } }); @@ -66,7 +70,6 @@ function downloadMiningPoolLogos() { }) } -const poolsJsonUrl = 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'; let assetsJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.json'; let assetsMinimalJsonUrl = 'https://raw.githubusercontent.com/mempool/asset_registry_db/master/index.minimal.json'; @@ -82,8 +85,6 @@ console.log('Downloading assets'); download(PATH + 'assets.json', assetsJsonUrl); console.log('Downloading assets minimal'); download(PATH + 'assets.minimal.json', assetsMinimalJsonUrl); -console.log('Downloading mining pools info'); -download(PATH + 'pools.json', poolsJsonUrl); console.log('Downloading testnet assets'); download(PATH + 'assets-testnet.json', testnetAssetsJsonUrl); console.log('Downloading testnet assets minimal'); diff --git a/nginx.conf b/nginx.conf index e9bd6034c..4db19d130 100644 --- a/nginx.conf +++ b/nginx.conf @@ -106,6 +106,7 @@ http { ~*^vi vi; ~*^zh zh; ~*^hi hi; + ~*^lt lt; } map $cookie_lang $lang { @@ -141,6 +142,7 @@ http { ~*^vi vi; ~*^zh zh; ~*^hi hi; + ~*^lt lt; } server { diff --git a/production/bitcoin.conf b/production/bitcoin.conf index 0fa1e943f..99fbaeed1 100644 --- a/production/bitcoin.conf +++ b/production/bitcoin.conf @@ -36,6 +36,12 @@ zmqpubrawtx=tcp://127.0.0.1:8335 #addnode=[2401:b140:2::92:204]:8333 #addnode=[2401:b140:2::92:205]:8333 #addnode=[2401:b140:2::92:206]:8333 +#addnode=[2401:b140:3::92:201]:8333 +#addnode=[2401:b140:3::92:202]:8333 +#addnode=[2401:b140:3::92:203]:8333 +#addnode=[2401:b140:3::92:204]:8333 +#addnode=[2401:b140:3::92:205]:8333 +#addnode=[2401:b140:3::92:206]:8333 [test] daemon=1 @@ -57,6 +63,12 @@ zmqpubrawtx=tcp://127.0.0.1:18335 #addnode=[2401:b140:2::92:204]:18333 #addnode=[2401:b140:2::92:205]:18333 #addnode=[2401:b140:2::92:206]:18333 +#addnode=[2401:b140:3::92:201]:18333 +#addnode=[2401:b140:3::92:202]:18333 +#addnode=[2401:b140:3::92:203]:18333 +#addnode=[2401:b140:3::92:204]:18333 +#addnode=[2401:b140:3::92:205]:18333 +#addnode=[2401:b140:3::92:206]:18333 [signet] daemon=1 @@ -78,3 +90,9 @@ zmqpubrawtx=tcp://127.0.0.1:38335 #addnode=[2401:b140:2::92:204]:38333 #addnode=[2401:b140:2::92:205]:38333 #addnode=[2401:b140:2::92:206]:38333 +#addnode=[2401:b140:3::92:201]:38333 +#addnode=[2401:b140:3::92:202]:38333 +#addnode=[2401:b140:3::92:203]:38333 +#addnode=[2401:b140:3::92:204]:38333 +#addnode=[2401:b140:3::92:205]:38333 +#addnode=[2401:b140:3::92:206]:38333 diff --git a/production/install b/production/install index f4426560d..240af8dfc 100755 --- a/production/install +++ b/production/install @@ -251,6 +251,7 @@ MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_HOME=/mempool MEMPOOL_USER=mempool MEMPOOL_GROUP=mempool +MEMPOOL_MYSQL_CREDENTIALS="${MEMPOOL_HOME}/.mysql_credentials" # name of Tor hidden service in torrc MEMPOOL_TOR_HS=mempool @@ -1009,6 +1010,7 @@ osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_ osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-build-all upgrade osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-kill-all stop osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-start-all start +osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-reset-all reset case $OS in @@ -1303,9 +1305,9 @@ if [ "${ELEMENTS_ELECTRS_INSTALL}" = ON ];then osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true fi -##################################### -# Core Lightning for Bitcoin Mainnet # -##################################### +############################## +# Core Lightning for Bitcoin # +############################## echo "[*] Installing Core Lightning" case $OS in @@ -1319,20 +1321,28 @@ case $OS in osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}" osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning" "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}" osSudo "${ROOT_USER}" chown -R "${CLN_USER}:${CLN_GROUP}" "${CLN_HOME}" + echo "[*] Creating symlink to .bitcoin folder" + osSudo "${CLN_USER}" ln -s "${BITCOIN_HOME}/.bitcoin" "${CLN_HOME}/.bitcoin" echo "[*] Installing Core Lightning package" osPackageInstall ${CLN_PKG} echo "[*] Installing Core Lightning mainnet Cronjob" - crontab_cln+='@reboot sleep 60 ; screen -dmS main lightningd --alias `hostname` --fee-base 0 --bitcoin-datadir /bitcoin\n' - crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --fee-base 0 --bitcoin-datadir /bitcoin --network testnet\n' - crontab_cln+='@reboot sleep 120 ; screen -dmS sig lightningd --alias `hostname` --fee-base 0 --bitcoin-datadir /bitcoin --network signet\n' + public_ipv4=$( ifconfig | grep 'inet ' | awk -F ' ' '{ print $2 }' | grep -v '^103\.165\.192\.' | grep -v '^127\.0\.0\.1' ) + public_ipv6=$( ifconfig | grep 'inet6' | awk -F ' ' '{ print $2 }' | grep -v '^2001:df6:7280::' | grep -v '^fe80::' | grep -v '^::1' ) + + crontab_cln+="@reboot sleep 60 ; screen -dmS main lightningd --rpc-file-mode 0660 --alias `hostname` --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n" + crontab_cln+="@reboot sleep 90 ; screen -dmS tes lightningd --rpc-file-mode 0660 --alias `hostname` --network testnet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n" + crontab_cln+="@reboot sleep 120 ; screen -dmS sig lightningd --rpc-file-mode 0660 --alias `hostname` --network signet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6 \n" + crontab_cln+="@reboot sleep 180 ; /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n" + crontab_cln+="1 * * * * /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n" echo "${crontab_cln}" | crontab -u "${CLN_USER}" - ;; Debian) ;; esac + ##################### # Bisq installation # ##################### @@ -1861,7 +1871,7 @@ grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by _EOF_ echo "[*] save MySQL credentials" -cat > ${MEMPOOL_HOME}/mysql_credentials << _EOF_ +cat > "${MEMPOOL_MYSQL_CREDENTIALS}" << _EOF_ declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}" declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}" declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}" @@ -1881,6 +1891,7 @@ declare -x MEMPOOL_LIQUIDTESTNET_PASS="${MEMPOOL_LIQUIDTESTNET_PASS}" declare -x MEMPOOL_BISQ_USER="${MEMPOOL_BISQ_USER}" declare -x MEMPOOL_BISQ_PASS="${MEMPOOL_BISQ_PASS}" _EOF_ +chown "${MEMPOOL_USER}:${MEMPOOL_GROUP}" "${MEMPOOL_MYSQL_CREDENTIALS}" ##### nginx diff --git a/production/mempool-build-all b/production/mempool-build-all index 048aeefdc..a4cb650d7 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -12,7 +12,7 @@ ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2) ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2) # get mysql credentials -MYSQL_CRED_FILE=${HOME}/mempool/mysql_credentials +MYSQL_CRED_FILE=${HOME}/.mysql_credentials if [ -f "${MYSQL_CRED_FILE}" ];then . ${MYSQL_CRED_FILE} fi diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 525cdb115..e612c0440 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -9,5 +9,6 @@ "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", - "ITEMS_PER_PAGE": 25 + "ITEMS_PER_PAGE": 25, + "LIGHTNING": true } diff --git a/production/mempool-reset-all b/production/mempool-reset-all new file mode 100755 index 000000000..22f004610 --- /dev/null +++ b/production/mempool-reset-all @@ -0,0 +1,3 @@ +#!/usr/bin/env zsh +rm $HOME/*/backend/mempool-config.json +rm $HOME/*/frontend/mempool-frontend-config.json diff --git a/production/nginx/http-language.conf b/production/nginx/http-language.conf index 11c3eff33..599ee5d74 100644 --- a/production/nginx/http-language.conf +++ b/production/nginx/http-language.conf @@ -31,6 +31,7 @@ map $http_accept_language $header_lang { ~*^uk uk; ~*^vi vi; ~*^zh zh; + ~*^lt lt; } map $cookie_lang $lang { default $header_lang; @@ -65,4 +66,5 @@ map $cookie_lang $lang { ~*^uk uk; ~*^vi vi; ~*^zh zh; + ~*^lt lt; }