diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0602e916a..c435d6ea5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,96 @@ jobs: run: npm run build working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend + + cache: + name: "Cache assets for builds" + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + path: assets + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + registry-url: "https://registry.npmjs.org" + + - name: Install (Prod dependencies only) + run: npm ci --omit=dev --omit=optional + working-directory: assets/frontend + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video + + # - name: Unzip assets before building (dist) + # continue-on-error: true + # run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources + + - name: Sync-assets + run: npm run sync-assets-dev + working-directory: assets/frontend + + - name: Zip mining-pool assets + run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/* + + - name: Zip promo-video assets + run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/* + + - name: Upload mining pool assets as artifact + uses: actions/upload-artifact@v4 + with: + name: mining-pool-assets + path: mining-pool-assets.zip + + - name: Upload promo video assets as artifact + uses: actions/upload-artifact@v4 + with: + name: promo-video-assets + path: promo-video-assets.zip + + - name: Save mining pool assets cache + id: cache-mining-pool-save + uses: actions/cache/save@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Save promo video assets cache + id: cache-promo-video-save + uses: actions/cache/save@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + frontend: + needs: cache if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" strategy: matrix: @@ -103,9 +192,141 @@ jobs: # - name: Test # run: npm run test + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: mining-pool-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o mining-pool-assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/src/resources/mining-pools + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: promo-video-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o promo-video-assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/src/resources/promo-video + + # - name: Unzip assets before building (dist) + # run: unzip assets.zip -d ${{ matrix.node }}/${{ matrix.flavor }}/frontend/dist/mempool/browser/resources + + - name: Display resulting source tree + run: ls -R + - name: Build run: npm run build working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + e2e: + if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + runs-on: "ubuntu-latest" + needs: frontend + strategy: + fail-fast: false + matrix: + # module: ["mempool", "liquid", "bisq"] Disabling bisq support for now + module: ["mempool", "liquid"] + include: + - module: "mempool" + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts + cypress/e2e/testnet/*.spec.ts + - module: "liquid" + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + # - module: "bisq" + # spec: | + # cypress/e2e/bisq/bisq.spec.ts + name: E2E tests for ${{ matrix.module }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + path: ${{ matrix.module }} + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "npm" + cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore cached promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: mining-pool-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: promo-video-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video + + - name: Chrome browser tests (${{ matrix.module }}) + 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-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 }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + \ No newline at end of file diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml deleted file mode 100644 index f12aebe8b..000000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Cypress Tests - -on: - push: - branches: [master] - pull_request: - types: [opened, synchronize] - -jobs: - cypress: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: "ubuntu-latest" - strategy: - fail-fast: false - matrix: - module: ["mempool", "liquid", "bisq"] - include: - - module: "mempool" - spec: | - cypress/e2e/mainnet/*.spec.ts - cypress/e2e/signet/*.spec.ts - cypress/e2e/testnet/*.spec.ts - - module: "liquid" - spec: | - cypress/e2e/liquid/liquid.spec.ts - cypress/e2e/liquidtestnet/liquidtestnet.spec.ts - - module: "bisq" - spec: | - cypress/e2e/bisq/bisq.spec.ts - - name: E2E tests for ${{ matrix.module }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - path: ${{ matrix.module }} - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: 20 - cache: "npm" - cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json - - - name: Chrome browser tests (${{ matrix.module }}) - 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-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 }}" - env: - COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 76b27d630..3cb79b909 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -33,7 +33,8 @@ "DISK_CACHE_BLOCK_INTERVAL": 6, "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, "ALLOW_UNREACHABLE": true, - "PRICE_UPDATES_PER_HOUR": 1 + "PRICE_UPDATES_PER_HOUR": 1, + "MAX_TRACKED_ADDRESSES": 100 }, "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 0c30651ce..9445fc25d 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -34,7 +34,8 @@ "DISK_CACHE_BLOCK_INTERVAL": 999, "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, "ALLOW_UNREACHABLE": true, - "PRICE_UPDATES_PER_HOUR": 1 + "PRICE_UPDATES_PER_HOUR": 1, + "MAX_TRACKED_ADDRESSES": 1 }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 2991162e9..97c218370 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -48,6 +48,7 @@ describe('Mempool Backend Config', () => { MAX_PUSH_TX_SIZE_WEIGHT: 400000, ALLOW_UNREACHABLE: true, PRICE_UPDATES_PER_HOUR: 1, + MAX_TRACKED_ADDRESSES: 1, }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 5bd961e23..02640efc0 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,3 +1,4 @@ +import { IBitcoinApi } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 6b4a14a0e..21818dc62 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -2,7 +2,7 @@ import config from '../config'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import logger from '../logger'; import memPool from './mempool'; -import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended } from '../mempool.interfaces'; +import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces'; import { Common } from './common'; import diskCache from './disk-cache'; import transactionUtils from './transaction-utils'; @@ -201,7 +201,8 @@ class Blocks { txid: tx.txid, vsize: tx.weight / 4, fee: tx.fee ? Math.round(tx.fee * 100000000) : 0, - value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000) + value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) * 100000000), + flags: 0, }; }); @@ -214,7 +215,7 @@ class Blocks { public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { return { id: hash, - transactions: Common.stripTransactions(transactions), + transactions: Common.classifyTransactions(transactions), }; } @@ -560,6 +561,121 @@ class Blocks { logger.debug(`Indexing block audit details completed`); } + /** + * [INDEXING] Index transaction classification flags for Goggles + */ + public async $classifyBlocks(): Promise { + // classification requires an esplora backend + if (!Common.blocksSummariesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') { + return; + } + + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); + const currentBlockHeight = blockchainInfo.blocks; + + const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0); + const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0); + + // nothing to do + if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { + return; + } + + let timer = Date.now(); + let indexedThisRun = 0; + let indexedTotal = 0; + + const minHeight = Math.min( + unclassifiedBlocksList[unclassifiedBlocksList.length - 1]?.height ?? Infinity, + unclassifiedTemplatesList[unclassifiedTemplatesList.length - 1]?.height ?? Infinity, + ); + const numToIndex = Math.max( + unclassifiedBlocksList.length, + unclassifiedTemplatesList.length, + ); + + const unclassifiedBlocks = {}; + const unclassifiedTemplates = {}; + for (const block of unclassifiedBlocksList) { + unclassifiedBlocks[block.height] = block.id; + } + for (const template of unclassifiedTemplatesList) { + unclassifiedTemplates[template.height] = template.id; + } + + logger.debug(`Classifying blocks and templates from #${currentBlockHeight} to #${minHeight}`, logger.tags.goggles); + + for (let height = currentBlockHeight; height >= 0; height--) { + try { + let txs: TransactionExtended[] | null = null; + if (unclassifiedBlocks[height]) { + const blockHash = unclassifiedBlocks[height]; + // fetch transactions + txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || []; + // add CPFP + const cpfpSummary = Common.calculateCpfp(height, txs, true); + // classify + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1); + } + if (unclassifiedTemplates[height]) { + // classify template + const blockHash = unclassifiedTemplates[height]; + const template = await BlocksSummariesRepository.$getTemplate(blockHash); + const alreadyClassified = template?.transactions?.reduce((classified, tx) => (classified || tx.flags > 0), false); + let classifiedTemplate = template?.transactions || []; + if (!alreadyClassified) { + const templateTxs: (TransactionExtended | TransactionClassified)[] = []; + const blockTxMap: { [txid: string]: TransactionExtended } = {}; + for (const tx of (txs || [])) { + blockTxMap[tx.txid] = tx; + } + for (const templateTx of (template?.transactions || [])) { + let tx: TransactionExtended | null = blockTxMap[templateTx.txid]; + if (!tx) { + try { + tx = await transactionUtils.$getTransactionExtended(templateTx.txid, false, true, false); + } catch (e) { + // transaction probably not found + } + } + templateTxs.push(tx || templateTx); + } + const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true); + // classify + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; + for (const tx of classifiedTxs) { + classifiedTxMap[tx.txid] = tx; + } + classifiedTemplate = classifiedTemplate.map(tx => { + if (classifiedTxMap[tx.txid]) { + tx.flags = classifiedTxMap[tx.txid].flags || 0; + } + return tx; + }); + } + await BlocksSummariesRepository.$saveTemplate({ height, template: { id: blockHash, transactions: classifiedTemplate }, version: 1 }); + } + } catch (e) { + logger.warn(`Failed to classify template or block summary at ${height}`, logger.tags.goggles); + } + + // timing & logging + if (unclassifiedBlocks[height] || unclassifiedTemplates[height]) { + indexedThisRun++; + indexedTotal++; + } + const elapsedSeconds = (Date.now() - timer) / 1000; + if (elapsedSeconds > 5) { + const perSecond = indexedThisRun / elapsedSeconds; + logger.debug(`Classified #${height}: ${indexedTotal} / ${numToIndex} blocks (${perSecond.toFixed(1)}/s)`); + timer = Date.now(); + indexedThisRun = 0; + } + } + } + /** * [INDEXING] Index all blocks metadata for the mining dashboard */ @@ -945,7 +1061,7 @@ class Blocks { } public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, - skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise + skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise { if (skipMemoryCache === false) { // Check the memory cache @@ -965,6 +1081,7 @@ class Blocks { let height = blockHeight; let summary: BlockSummary; + let summaryVersion = 0; if (cpfpSummary && !Common.isLiquid()) { summary = { id: hash, @@ -974,14 +1091,17 @@ class Blocks { fee: tx.fee || 0, vsize: tx.vsize, value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), - rate: tx.effectiveFeePerVsize + rate: tx.effectiveFeePerVsize, + flags: tx.flags || Common.getTransactionFlags(tx), }; }), }; + summaryVersion = 1; } else { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); summary = this.summarizeBlockTransactions(hash, txs); + summaryVersion = 1; } else { // Call Core RPC const block = await bitcoinClient.getBlock(hash, 2); @@ -996,7 +1116,7 @@ class Blocks { // Index the response if needed if (Common.blocksSummariesIndexingEnabled() === true) { - await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions); + await BlocksSummariesRepository.$saveTransactions(height, hash, summary.transactions, summaryVersion); } return summary.transactions; @@ -1112,16 +1232,18 @@ class Blocks { if (cleanBlock.fee_amt_percentiles === null) { let summary; + let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); + summaryVersion = 1; } else { // Call Core RPC const block = await bitcoinClient.getBlock(cleanBlock.hash, 2); summary = this.summarizeBlock(block); } - await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions); + await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions, summaryVersion); cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash); } if (cleanBlock.fee_amt_percentiles !== null) { diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index af93b9622..52fa9042b 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,10 +1,9 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { Request } from 'express'; -import { Ancestor, CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; +import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; import config from '../config'; import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; -import rbfCache from './rbf-cache'; import transactionUtils from './transaction-utils'; import { isPoint } from '../utils/secp256k1'; export class Common { @@ -349,14 +348,18 @@ export class Common { } static classifyTransaction(tx: TransactionExtended): TransactionClassified { - const flags = this.getTransactionFlags(tx); + const flags = Common.getTransactionFlags(tx); tx.flags = flags; return { - ...this.stripTransaction(tx), + ...Common.stripTransaction(tx), flags, }; } + static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { + return txs.map(Common.classifyTransaction); + } + static stripTransaction(tx: TransactionExtended): TransactionStripped { return { txid: tx.txid, @@ -369,7 +372,7 @@ export class Common { } static stripTransactions(txs: TransactionExtended[]): TransactionStripped[] { - return txs.map(this.stripTransaction); + return txs.map(Common.stripTransaction); } static sleep$(ms: number): Promise { @@ -632,12 +635,12 @@ export class Common { } } - static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary { + static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root - const txMap = {}; + const txMap: { [txid: string]: TransactionExtended } = {}; // initialize the txMap for (const tx of transactions) { txMap[tx.txid] = tx; @@ -707,6 +710,15 @@ export class Common { } } } + if (saveRelatives) { + for (const cluster of clusters) { + cluster.txs.forEach((member, index) => { + txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse(); + txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse(); + txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize; + }); + } + } return { transactions, clusters, diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 89ef7a7be..162616af6 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 66; + private static currentVersion = 67; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -558,6 +558,14 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `statistics` ADD min_fee FLOAT UNSIGNED DEFAULT NULL'); await this.updateToSchemaVersion(66); } + + if (databaseSchemaVersion < 67 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)'); + await this.updateToSchemaVersion(67); + } } /** diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 0ca550f4c..58921fcfb 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,6 +1,6 @@ import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt'; import logger from '../logger'; -import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces'; +import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag, TransactionClassified } from '../mempool.interfaces'; import { Common, OnlineFeeStatsCalculator } from './common'; import config from '../config'; import { Worker } from 'worker_threads'; diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 937d4a7c5..d0e0b7fd8 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -24,6 +24,12 @@ import { ApiPrice } from '../repositories/PricesRepository'; import accelerationApi from './services/acceleration'; import mempool from './mempool'; +interface AddressTransactions { + mempool: MempoolTransactionExtended[], + confirmed: MempoolTransactionExtended[], + removed: MempoolTransactionExtended[], +} + // valid 'want' subscriptions const wantable = [ 'blocks', @@ -195,24 +201,49 @@ class WebsocketHandler { } if (parsedMessage && parsedMessage['track-address']) { - if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/ - .test(parsedMessage['track-address'])) { - let matchedAddress = parsedMessage['track-address']; - if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) { - matchedAddress = matchedAddress.toLowerCase(); - } - if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) { - client['track-address'] = '41' + matchedAddress + 'ac'; - } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) { - client['track-address'] = '21' + matchedAddress + 'ac'; - } else { - client['track-address'] = matchedAddress; - } + const validAddress = this.testAddress(parsedMessage['track-address']); + if (validAddress) { + client['track-address'] = validAddress; } else { client['track-address'] = null; } } + if (parsedMessage && parsedMessage['track-addresses'] && Array.isArray(parsedMessage['track-addresses'])) { + const addressMap: { [address: string]: string } = {}; + for (const address of parsedMessage['track-addresses']) { + const validAddress = this.testAddress(address); + if (validAddress) { + addressMap[address] = validAddress; + } + } + if (Object.keys(addressMap).length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) { + response['track-addresses-error'] = `"too many addresses requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} addresses"`; + client['track-addresses'] = null; + } else if (Object.keys(addressMap).length > 0) { + client['track-addresses'] = addressMap; + } else { + client['track-addresses'] = null; + } + } + + if (parsedMessage && parsedMessage['track-scriptpubkeys'] && Array.isArray(parsedMessage['track-scriptpubkeys'])) { + const spks: string[] = []; + for (const spk of parsedMessage['track-scriptpubkeys']) { + if (/^[a-fA-F0-9]+$/.test(spk)) { + spks.push(spk.toLowerCase()); + } + } + if (spks.length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) { + response['track-scriptpubkeys-error'] = `"too many scriptpubkeys requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} scriptpubkeys"`; + client['track-scriptpubkeys'] = null; + } else if (spks.length) { + client['track-scriptpubkeys'] = spks; + } else { + client['track-scriptpubkeys'] = null; + } + } + if (parsedMessage && parsedMessage['track-asset']) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { client['track-asset'] = parsedMessage['track-asset']; @@ -544,6 +575,50 @@ class WebsocketHandler { } } + if (client['track-addresses']) { + const addressMap: { [address: string]: AddressTransactions } = {}; + for (const [address, key] of Object.entries(client['track-addresses'] || {})) { + const newTransactions = Array.from(addressCache[key as string]?.values() || []); + const removedTransactions = Array.from(removedAddressCache[key as string]?.values() || []); + // txs may be missing prevouts in non-esplora backends + // so fetch the full transactions now + const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions; + if (fullTransactions?.length) { + addressMap[address] = { + mempool: fullTransactions, + confirmed: [], + removed: removedTransactions, + }; + } + } + + if (Object.keys(addressMap).length > 0) { + response['multi-address-transactions'] = JSON.stringify(addressMap); + } + } + + if (client['track-scriptpubkeys']) { + const spkMap: { [spk: string]: AddressTransactions } = {}; + for (const spk of client['track-scriptpubkeys'] || []) { + const newTransactions = Array.from(addressCache[spk as string]?.values() || []); + const removedTransactions = Array.from(removedAddressCache[spk as string]?.values() || []); + // txs may be missing prevouts in non-esplora backends + // so fetch the full transactions now + const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(newTransactions) : newTransactions; + if (fullTransactions?.length) { + spkMap[spk] = { + mempool: fullTransactions, + confirmed: [], + removed: removedTransactions, + }; + } + } + + if (Object.keys(spkMap).length > 0) { + response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; @@ -703,7 +778,8 @@ class WebsocketHandler { template: { id: block.id, transactions: stripped, - } + }, + version: 1, }); BlocksAuditsRepository.$saveAudit({ @@ -843,6 +919,42 @@ class WebsocketHandler { } } + if (client['track-addresses']) { + const addressMap: { [address: string]: AddressTransactions } = {}; + for (const [address, key] of Object.entries(client['track-addresses'] || {})) { + const fullTransactions = Array.from(addressCache[key as string]?.values() || []); + if (fullTransactions?.length) { + addressMap[address] = { + mempool: [], + confirmed: fullTransactions, + removed: [], + }; + } + } + + if (Object.keys(addressMap).length > 0) { + response['multi-address-transactions'] = JSON.stringify(addressMap); + } + } + + if (client['track-scriptpubkeys']) { + const spkMap: { [spk: string]: AddressTransactions } = {}; + for (const spk of client['track-scriptpubkeys'] || []) { + const fullTransactions = Array.from(addressCache[spk as string]?.values() || []); + if (fullTransactions?.length) { + spkMap[spk] = { + mempool: [], + confirmed: fullTransactions, + removed: [], + }; + } + } + + if (Object.keys(spkMap).length > 0) { + response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; @@ -912,6 +1024,28 @@ class WebsocketHandler { + '}'; } + // checks if an address conforms to a valid format + // returns the canonical form: + // - lowercase for bech32(m) + // - lowercase scriptpubkey for P2PK + // or false if invalid + private testAddress(address): string | false { + if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/.test(address)) { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) { + address = address.toLowerCase(); + } + if (/^04[a-fA-F0-9]{128}$/.test(address)) { + return '41' + address + 'ac'; + } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(address)) { + return '21' + address + 'ac'; + } else { + return address; + } + } else { + return false; + } + } + private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set } { const addressCache: { [address: string]: Set } = {}; for (const tx of transactions) { diff --git a/backend/src/config.ts b/backend/src/config.ts index 4115149e6..df1022a67 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -39,6 +39,7 @@ interface IConfig { MAX_PUSH_TX_SIZE_WEIGHT: number; ALLOW_UNREACHABLE: boolean; PRICE_UPDATES_PER_HOUR: number; + MAX_TRACKED_ADDRESSES: number; }; ESPLORA: { REST_API_URL: string; @@ -193,6 +194,7 @@ const defaults: IConfig = { 'MAX_PUSH_TX_SIZE_WEIGHT': 400000, 'ALLOW_UNREACHABLE': true, 'PRICE_UPDATES_PER_HOUR': 1, + 'MAX_TRACKED_ADDRESSES': 1, }, 'ESPLORA': { 'REST_API_URL': 'http://127.0.0.1:3000', diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index 2e2f8b037..90b4a59e6 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -185,6 +185,7 @@ class Indexer { await blocks.$generateCPFPDatabase(); await blocks.$generateAuditStats(); await auditReplicator.$sync(); + await blocks.$classifyBlocks(); } catch (e) { this.indexerRunning = false; logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); diff --git a/backend/src/logger.ts b/backend/src/logger.ts index 364c529e7..bbd781df6 100644 --- a/backend/src/logger.ts +++ b/backend/src/logger.ts @@ -35,6 +35,7 @@ class Logger { public tags = { mining: 'Mining', ln: 'Lightning', + goggles: 'Goggles', }; // @ts-ignore diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index c93372ded..ead0a84ad 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -280,7 +280,8 @@ export interface BlockExtended extends IEsploraApi.Block { export interface BlockSummary { id: string; - transactions: TransactionStripped[]; + transactions: TransactionClassified[]; + version?: number; } export interface AuditSummary extends BlockAudit { @@ -288,8 +289,8 @@ export interface AuditSummary extends BlockAudit { size?: number, weight?: number, tx_count?: number, - transactions: TransactionStripped[]; - template?: TransactionStripped[]; + transactions: TransactionClassified[]; + template?: TransactionClassified[]; } export interface BlockPrice { diff --git a/backend/src/replication/AuditReplication.ts b/backend/src/replication/AuditReplication.ts index 5de9de0da..503c61613 100644 --- a/backend/src/replication/AuditReplication.ts +++ b/backend/src/replication/AuditReplication.ts @@ -105,7 +105,8 @@ class AuditReplication { template: { id: blockHash, transactions: auditSummary.template || [] - } + }, + version: 1, }); await blocksAuditsRepository.$saveAudit({ hash: blockHash, diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index a798c40f8..a2a084265 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -1040,16 +1040,18 @@ class BlocksRepository { if (extras.feePercentiles === null) { let summary; + let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); + summaryVersion = 1; } else { // Call Core RPC const block = await bitcoinClient.getBlock(dbBlk.id, 2); summary = blocks.summarizeBlock(block); } - await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions); + await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions, summaryVersion); extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id); } if (extras.feePercentiles !== null) { diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index 09598db03..f85914e31 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -1,6 +1,6 @@ import DB from '../database'; import logger from '../logger'; -import { BlockSummary, TransactionStripped } from '../mempool.interfaces'; +import { BlockSummary, TransactionClassified } from '../mempool.interfaces'; class BlocksSummariesRepository { public async $getByBlockId(id: string): Promise { @@ -17,30 +17,31 @@ class BlocksSummariesRepository { return undefined; } - public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise { + public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionClassified[], version: number): Promise { try { const transactionsStr = JSON.stringify(transactions); await DB.query(` INSERT INTO blocks_summaries - SET height = ?, transactions = ?, id = ? - ON DUPLICATE KEY UPDATE transactions = ?`, - [blockHeight, transactionsStr, blockId, transactionsStr]); + SET height = ?, transactions = ?, id = ?, version = ? + ON DUPLICATE KEY UPDATE transactions = ?, version = ?`, + [blockHeight, transactionsStr, blockId, version, transactionsStr, version]); } catch (e: any) { logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`); throw e; } } - public async $saveTemplate(params: { height: number, template: BlockSummary}) { + public async $saveTemplate(params: { height: number, template: BlockSummary, version: number}): Promise { const blockId = params.template?.id; try { const transactions = JSON.stringify(params.template?.transactions || []); await DB.query(` - INSERT INTO blocks_templates (id, template) - VALUE (?, ?) + INSERT INTO blocks_templates (id, template, version) + VALUE (?, ?, ?) ON DUPLICATE KEY UPDATE - template = ? - `, [blockId, transactions, transactions]); + template = ?, + version = ? + `, [blockId, transactions, params.version, transactions, params.version]); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart logger.debug(`Cannot save block template for ${blockId} because it has already been indexed, ignoring`); @@ -57,6 +58,7 @@ class BlocksSummariesRepository { return { id: templates[0].id, transactions: JSON.parse(templates[0].template), + version: templates[0].version, }; } } catch (e) { @@ -76,6 +78,41 @@ class BlocksSummariesRepository { return []; } + public async $getSummariesWithVersion(version: number): Promise<{ height: number, id: string }[]> { + try { + const [rows]: any[] = await DB.query(` + SELECT + height, + id + FROM blocks_summaries + WHERE version = ? + ORDER BY height DESC;`, [version]); + return rows; + } catch (e) { + logger.err(`Cannot get block summaries with version. Reason: ` + (e instanceof Error ? e.message : e)); + } + + return []; + } + + public async $getTemplatesWithVersion(version: number): Promise<{ height: number, id: string }[]> { + try { + const [rows]: any[] = await DB.query(` + SELECT + blocks_summaries.height as height, + blocks_templates.id as id + FROM blocks_templates + JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id + WHERE blocks_templates.version = ? + ORDER BY height DESC;`, [version]); + return rows; + } catch (e) { + logger.err(`Cannot get block summaries with version. Reason: ` + (e instanceof Error ? e.message : e)); + } + + return []; + } + /** * Get the fee percentiles if the block has already been indexed, [] otherwise * diff --git a/contributors/isghe.txt b/contributors/isghe.txt new file mode 100644 index 000000000..5e556448a --- /dev/null +++ b/contributors/isghe.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 18, 2024. + +Signed: isghe diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index e73fa1929..c68e37baa 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -35,6 +35,7 @@ "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__ + "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ }, "CORE_RPC": { "HOST": "__CORE_RPC_HOST__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 232cf7284..d73ea83fb 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -36,6 +36,7 @@ __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6} __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000} __MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true} __MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1} +__MEMPOOL_MAX_TRACKED_ADDRESSES__=${MEMPOOL_MAX_TRACKED_ADDRESSES:=1} # CORE_RPC __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} @@ -188,6 +189,7 @@ sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INT sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json +sed -i "s!__MEMPOOL_MAX_TRACKED_ADDRESSES__!${__MEMPOOL_MAX_TRACKED_ADDRESSES__}!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/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts index 92ad9b744..8834d09e9 100644 --- a/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts +++ b/frontend/src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts @@ -30,7 +30,7 @@ export class BisqDashboardComponent implements OnInit { ngOnInit(): void { this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`); - this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more.`); + this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.`); this.websocketService.want(['blocks']); this.volumes$ = this.bisqApiService.getAllVolumesDay$() diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index dd2a4ead2..054f26fe6 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -405,7 +405,7 @@

- The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html index 9ae0ddade..98095aa07 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html @@ -29,7 +29,7 @@

-
Out-of-band Fees Per Block
+
Total Bid Boost
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index a51476dca..d348449fe 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -68,7 +68,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`); + this.seoService.setTitle($localize`:@@bcf34abc2d9ed8f45a2f65dd464c46694e9a181e:Acceleration Fees`); this.isLoading = true; if (this.widget) { this.miningWindowPreference = '1m'; @@ -83,7 +83,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { }), map(([accelerations, blockFeesResponse]) => { return { - avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length + avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length }; }), ); @@ -153,7 +153,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { while (last <= val.avgHeight) { blockCount++; totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0); - totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0); + totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0); totalCount += (blockAccelerations[last] || []).length; last++; } @@ -248,7 +248,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { icon: 'roundRect', }, { - name: 'Out-of-band fees per block', + name: 'Total bid boost per block', inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -258,7 +258,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { ], selected: { 'In-band fees per block': false, - 'Out-of-band fees per block': true, + 'Total bid boost per block': true, }, show: !this.widget, }, @@ -301,7 +301,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { { legendHoverLink: false, zlevel: 1, - name: 'Out-of-band fees per block', + name: 'Total bid boost per block', data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]), stack: 'Total', type: 'bar', diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html index 21cd57ae0..5e8aa729a 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html @@ -1,14 +1,14 @@
-
Transactions
+
Requests
{{ stats.count }}
accelerated
-
Out-of-band Fees
+
Total Bid Boost
{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} BTC
@@ -17,7 +17,7 @@
-
Success rate
+
Success Rate
{{ stats.successRate.toFixed(2) }} %
mined
@@ -29,21 +29,21 @@
-
Transactions
+
Requests
-
Out-of-band Fees
+
Total Bid Boost
-
Success rate
+
Success Rate
diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts index d83303619..0a6ef065c 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts @@ -27,11 +27,11 @@ export class AccelerationStatsComponent implements OnInit { let totalFeesPaid = 0; let totalSucceeded = 0; let totalCanceled = 0; - for (const acceleration of accelerations) { - if (acceleration.status === 'completed') { + for (const acc of accelerations) { + if (acc.status === 'completed') { totalSucceeded++; - totalFeesPaid += acceleration.feePaid || 0; - } else if (acceleration.status === 'failed') { + totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0; + } else if (acc.status === 'failed') { totalCanceled++; } } diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index f2265282f..9a919ca54 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -14,7 +14,7 @@ Requested - Out-of-band Fee + Bid Boost Block Status @@ -39,7 +39,7 @@ - {{ (acceleration.feePaid) | number }} sat + {{ (acceleration.boost) | number }} sat ~ @@ -48,7 +48,7 @@ {{ acceleration.blockHeight }} - Pending + Pending Mined Canceled diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index d53de7c1d..c1ab011ea 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -49,6 +49,9 @@ export class AccelerationsListComponent implements OnInit { acceleration.status = acceleration.status || 'accelerating'; } } + for (const acc of accelerations) { + acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; + } if (this.widget) { return of(accelerations.slice(-6).reverse()); } else { diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html index 91b721db6..243a48939 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html @@ -7,7 +7,7 @@
- Active accelerations + Active Accelerations
@@ -69,7 +69,7 @@
diff --git a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html index c94bbf43a..377f8754a 100644 --- a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html +++ b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html @@ -1,10 +1,10 @@
-
Transactions
+
Requests
{{ stats.count }}
-
accelerated
+
pending
@@ -17,7 +17,7 @@
-
Total vsize
+
Total Vsize
{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% of next block
@@ -29,7 +29,7 @@
-
Transactions
+
Requests
@@ -43,7 +43,7 @@
-
Total vsize
+
Total Vsize
diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index 7b1c2f9e5..f60b04cdd 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -18,7 +18,7 @@
{{ group.label }}
- +
diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index ce0dd76ab..9951984df 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,5 +1,7 @@ -import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; +import { StateService } from '../../services/state.service'; +import { Subscription } from 'rxjs'; @Component({ @@ -7,24 +9,48 @@ import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; templateUrl: './block-filters.component.html', styleUrls: ['./block-filters.component.scss'], }) -export class BlockFiltersComponent implements OnChanges { +export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { @Input() cssWidth: number = 800; + @Input() excludeFilters: string[] = []; @Output() onFilterChanged: EventEmitter = new EventEmitter(); + filterSubscription: Subscription; + filters = TransactionFilters; filterGroups = FilterGroups; + disabledFilters: { [key: string]: boolean } = {}; activeFilters: string[] = []; filterFlags: { [key: string]: boolean } = {}; menuOpen: boolean = false; constructor( + private stateService: StateService, private cd: ChangeDetectorRef, ) {} + ngOnInit(): void { + this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => { + for (const key of Object.keys(this.filterFlags)) { + this.filterFlags[key] = false; + } + for (const key of activeFilters) { + this.filterFlags[key] = !this.disabledFilters[key]; + } + this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])]; + this.onFilterChanged.emit(this.getBooleanFlags()); + }); + } + ngOnChanges(changes: SimpleChanges): void { if (changes.cssWidth) { this.cd.markForCheck(); } + if (changes.excludeFilters) { + this.disabledFilters = {}; + this.excludeFilters.forEach(filter => { + this.disabledFilters[filter] = true; + }); + } } toggleFilter(key): void { @@ -46,7 +72,9 @@ export class BlockFiltersComponent implements OnChanges { // remove active filter this.activeFilters = this.activeFilters.filter(f => f != key); } - this.onFilterChanged.emit(this.getBooleanFlags()); + const booleanFlags = this.getBooleanFlags(); + this.onFilterChanged.emit(booleanFlags); + this.stateService.activeGoggles$.next([...this.activeFilters]); } getBooleanFlags(): bigint | null { @@ -67,4 +95,8 @@ export class BlockFiltersComponent implements OnChanges { } return true; } + + ngOnDestroy(): void { + this.filterSubscription.unsubscribe(); + } } \ No newline at end of file diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html index 9f5e7cb47..9d27d8d90 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html @@ -13,6 +13,6 @@ [auditEnabled]="auditHighlighting" [blockConversion]="blockConversion" > - +
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 8a449a121..d6000e27b 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 @@ -40,6 +40,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() unavailable: boolean = false; @Input() auditHighlighting: boolean = false; @Input() showFilters: boolean = false; + @Input() excludeFilters: string[] = []; @Input() filterFlags: bigint | null = null; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @@ -71,6 +72,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On searchText: string; searchSubscription: Subscription; + filtersAvailable: boolean = true; + activeFilterFlags: bigint | null = null; constructor( readonly ngZone: NgZone, @@ -110,16 +113,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (changes.overrideColor && this.scene) { this.scene.setColorFunction(this.overrideColors); } - if ((changes.filterFlags || changes.showFilters) && this.scene) { - this.setFilterFlags(this.filterFlags); + if ((changes.filterFlags || changes.showFilters)) { + this.setFilterFlags(); } } - setFilterFlags(flags: bigint | null): void { - if (flags != null) { - this.scene.setColorFunction(this.getFilterColorFunction(flags)); - } else { - this.scene.setColorFunction(this.overrideColors); + setFilterFlags(flags?: bigint | null): void { + this.activeFilterFlags = this.filterFlags || flags || null; + if (this.scene) { + if (flags != null) { + this.scene.setColorFunction(this.getFilterColorFunction(flags)); + } else { + this.scene.setColorFunction(this.overrideColors); + } } this.start(); } @@ -150,6 +156,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On // initialize the scene without any entry transition setup(transactions: TransactionStripped[]): void { + this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); if (this.scene) { this.scene.setup(transactions); this.readyNextFrame = true; @@ -260,7 +267,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, - colorFunction: this.overrideColors }); + colorFunction: this.getColorFunction() }); this.start(); } } @@ -504,6 +511,16 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.txHoverEvent.emit(hoverId); } + getColorFunction(): ((tx: TxView) => Color) { + if (this.filterFlags) { + return this.getFilterColorFunction(this.filterFlags); + } else if (this.activeFilterFlags) { + return this.getFilterColorFunction(this.activeFilterFlags); + } else { + return this.overrideColors; + } + } + getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { return (tx: TxView) => { if ((tx.bigintFlags & flags) === flags) { diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 1e6edd2c8..89699a68c 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -59,7 +59,7 @@ - Health  + Health @@ -229,7 +231,8 @@
+ (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit" + [showFilters]="true" [excludeFilters]="['replacement']">
@@ -239,11 +242,12 @@
-

Actual Block

+

Actual Block

+ (txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit" + [showFilters]="true" [excludeFilters]="['replacement']">
diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index f31e262ce..7d8b37d00 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -48,9 +48,13 @@