mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 14:50:52 +01:00
Merge branch 'natsee/liquid-federation-audit' into natsee/liquid-audit-dashboard
This commit is contained in:
commit
c957692726
83 changed files with 23198 additions and 9943 deletions
221
.github/workflows/ci.yml
vendored
221
.github/workflows/ci.yml
vendored
|
@ -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 }}
|
||||
|
64
.github/workflows/cypress.yml
vendored
64
.github/workflows/cypress.yml
vendored
|
@ -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 }}
|
|
@ -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",
|
||||
|
|
|
@ -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__",
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
|
|
|
@ -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<void> {
|
||||
// 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<TransactionStripped[]>
|
||||
skipDBLookup = false, cpfpSummary?: CpfpSummary, blockHeight?: number): Promise<TransactionClassified[]>
|
||||
{
|
||||
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) {
|
||||
|
|
|
@ -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<void> {
|
||||
|
@ -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,
|
||||
|
|
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 67;
|
||||
private static currentVersion = 68;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
|
@ -559,8 +559,15 @@ class DatabaseMigration {
|
|||
await this.updateToSchemaVersion(66);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 67 && config.MEMPOOL.NETWORK === "liquid") {
|
||||
// Drop and re-create the elements_pegs table
|
||||
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);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 68 && config.MEMPOOL.NETWORK === "liquid") {
|
||||
await this.$executeQuery('TRUNCATE TABLE elements_pegs');
|
||||
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
|
||||
await this.$executeQuery(`UPDATE state SET number = 0 WHERE name = 'last_elements_block';`);
|
||||
|
@ -571,7 +578,7 @@ class DatabaseMigration {
|
|||
// Create the federation_txos table that uses the federation_addresses table as a foreign key
|
||||
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
|
||||
await this.$executeQuery(`INSERT INTO state VALUES('last_bitcoin_block_audit', 0, NULL);`);
|
||||
await this.updateToSchemaVersion(67);
|
||||
await this.updateToSchemaVersion(68);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<MempoolTransactionExtended> } {
|
||||
const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
|
||||
for (const tx of transactions) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -35,6 +35,7 @@ class Logger {
|
|||
public tags = {
|
||||
mining: 'Mining',
|
||||
ln: 'Lightning',
|
||||
goggles: 'Goggles',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -105,7 +105,8 @@ class AuditReplication {
|
|||
template: {
|
||||
id: blockHash,
|
||||
transactions: auditSummary.template || []
|
||||
}
|
||||
},
|
||||
version: 1,
|
||||
});
|
||||
await blocksAuditsRepository.$saveAudit({
|
||||
hash: blockHash,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<BlockSummary | undefined> {
|
||||
|
@ -17,30 +17,31 @@ class BlocksSummariesRepository {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
|
||||
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionClassified[], version: number): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
*
|
||||
|
|
3
contributors/isghe.txt
Normal file
3
contributors/isghe.txt
Normal file
|
@ -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
|
|
@ -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__",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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$()
|
||||
|
|
|
@ -405,7 +405,7 @@
|
|||
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright © 2019-2023<br>
|
||||
Copyright © 2019-2024<br>
|
||||
Mempool Space K.K.<br>
|
||||
and other shadowy super-coders
|
||||
</div>
|
||||
|
@ -422,7 +422,7 @@
|
|||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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 <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
|
|
@ -66,7 +66,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';
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.successRate.toFixed(2) }} %</div>
|
||||
<div class="symbol" i18n="accelerator.mined-next-block">mined</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success rate</h5>
|
||||
<h5 class="card-title" i18n="accelerator.success-rate">Success Rate</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<!-- pending stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active accelerations</span>
|
||||
<span [attr.data-cy]="'pending-accelerations'" i18n="accelerator.pending-accelerations">Active Accelerations</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card">
|
||||
|
@ -69,7 +69,7 @@
|
|||
<div class="card list-card">
|
||||
<div class="card-body">
|
||||
<div class="title-link">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Active Accelerations</h5>
|
||||
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
|
||||
</div>
|
||||
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list>
|
||||
</div>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div>
|
||||
<div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5>
|
||||
<h5 class="card-title" i18n="accelerator.total-vsize">Total Vsize</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<h5>{{ group.label }}</h5>
|
||||
<div class="filter-group">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
<button *ngIf="!disabledFilters[filter.key]" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
@ -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<bigint | null> = 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();
|
||||
}
|
||||
}
|
|
@ -13,6 +13,6 @@
|
|||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
<app-block-filters *ngIf="showFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
<app-block-filters *ngIf="showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="auditAvailable">
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td><ng-container i18n="latest-blocks.health">Health</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||
<td>
|
||||
<span
|
||||
class="health-badge badge"
|
||||
|
@ -115,6 +115,8 @@
|
|||
[orientation]="'top'"
|
||||
[flip]="false"
|
||||
[blockConversion]="blockConversion"
|
||||
[showFilters]="true"
|
||||
[excludeFilters]="['replacement']"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
|
@ -229,7 +231,8 @@
|
|||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
|
@ -239,11 +242,12 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home" *ngIf="stateService.env.ACCELERATOR">
|
||||
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.acceleration-dashboard" title="Acceleration Dashboard"></fa-icon></a>
|
||||
<a class="nav-link" [routerLink]="['/acceleration' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'rocket']" [fixedWidth]="true" i18n-title="master-page.accelerator-dashboard" title="Accelerator Dashboard"></fa-icon></a>
|
||||
</li>
|
||||
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
:host ::ng-deep {
|
||||
.dropdown-item {
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 34px);
|
||||
}
|
||||
.dropdown-menu {
|
||||
width: calc(100% - 34px);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.dropdown-item {
|
||||
width: 410px;
|
||||
}
|
||||
.dropdown-menu {
|
||||
width: 410px;
|
||||
}
|
||||
|
|
|
@ -170,6 +170,7 @@ export class SearchFormComponent implements OnInit {
|
|||
addresses: [],
|
||||
nodes: [],
|
||||
channels: [],
|
||||
liquidAsset: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -187,6 +188,7 @@ export class SearchFormComponent implements OnInit {
|
|||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
let matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
|
||||
const liquidAsset = this.assets ? (this.assets[searchText] || []) : [];
|
||||
|
||||
// Add B prefix to addresses in Bisq network
|
||||
if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) {
|
||||
|
@ -211,6 +213,7 @@ export class SearchFormComponent implements OnInit {
|
|||
otherNetworks: otherNetworks,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
liquidAsset: liquidAsset,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -259,16 +262,16 @@ export class SearchFormComponent implements OnInit {
|
|||
} else if (this.regexTransaction.test(searchText)) {
|
||||
const matches = this.regexTransaction.exec(searchText);
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
if (this.assets[matches[1]]) {
|
||||
this.navigate('/assets/asset/', matches[1]);
|
||||
if (this.assets[matches[0]]) {
|
||||
this.navigate('/assets/asset/', matches[0]);
|
||||
}
|
||||
this.electrsApiService.getAsset$(matches[1])
|
||||
this.electrsApiService.getAsset$(matches[0])
|
||||
.subscribe(
|
||||
() => { this.navigate('/assets/asset/', matches[1]); },
|
||||
() => { this.navigate('/assets/asset/', matches[0]); },
|
||||
() => {
|
||||
this.electrsApiService.getBlock$(matches[1])
|
||||
this.electrsApiService.getBlock$(matches[0])
|
||||
.subscribe(
|
||||
(block) => { this.navigate('/block/', matches[1], { state: { data: { block } } }); },
|
||||
(block) => { this.navigate('/block/', matches[0], { state: { data: { block } } }); },
|
||||
() => { this.navigate('/tx/', matches[0]); });
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAsset.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div>
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">{{ networkName }} Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
|
@ -17,20 +17,20 @@
|
|||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">Bitcoin Transaction</div>
|
||||
<ng-template [ngIf]="results.txId && !results.liquidAsset.length">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">{{ networkName }} Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address">
|
||||
<div class="card-title" i18n="search.bitcoin-address">Bitcoin Address</div>
|
||||
<div class="card-title" i18n="search.bitcoin-address">{{ networkName }} Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 20 : 30 }"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 17 : 30 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.blockHash">
|
||||
<div class="card-title" i18n="search.bitcoin-block">Bitcoin Block</div>
|
||||
<div class="card-title" i18n="search.bitcoin-block">{{ networkName }} Block</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
|
@ -39,12 +39,12 @@
|
|||
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
|
||||
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 20 : 25 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 12 : 20 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div>
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">{{ networkName }} Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
|
@ -67,6 +67,12 @@
|
|||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.liquidAsset.length">
|
||||
<div class="card-title" i18n="search.liquid-asset">Liquid Asset</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 11 }"></ng-container> <b>({{ results.liquidAsset[1] }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-template #goTo let-x i18n="search.go-to">Go to "{{ x }}"</ng-template>
|
||||
|
|
|
@ -10,15 +10,20 @@ export class SearchResultsComponent implements OnChanges {
|
|||
@Input() results: any = {};
|
||||
@Output() selectedResult = new EventEmitter();
|
||||
|
||||
isMobile = (window.innerWidth <= 767.98);
|
||||
isMobile = (window.innerWidth <= 1150);
|
||||
resultsFlattened = [];
|
||||
activeIdx = 0;
|
||||
focusFirst = true;
|
||||
networkName = '';
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.networkName = this.stateService.network.charAt(0).toUpperCase() + this.stateService.network.slice(1);
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
|
|
|
@ -315,7 +315,7 @@
|
|||
|
||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||
|
||||
<p>"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, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
|
||||
<p>"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, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
|
||||
<li>What to Do When You See Abuse</li>
|
||||
|
||||
<br>
|
||||
|
|
|
@ -299,7 +299,7 @@
|
|||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="adjustedVsize != null">
|
||||
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize
|
||||
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
|
||||
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
|
@ -325,7 +325,7 @@
|
|||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="sigops != null">
|
||||
<td i18n="transaction.sigops|Transaction Sigops">Sigops
|
||||
<td><ng-container i18n="transaction.sigops|Transaction Sigops">Sigops</ng-container>
|
||||
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
|
|
|
@ -27,6 +27,8 @@ export interface WebsocketResponse {
|
|||
fees?: Recommendedfees;
|
||||
'track-tx'?: string;
|
||||
'track-address'?: string;
|
||||
'track-addresses'?: string[];
|
||||
'track-scriptpubkeys'?: string[];
|
||||
'track-asset'?: string;
|
||||
'track-mempool-block'?: number;
|
||||
'track-rbf'?: string;
|
||||
|
|
|
@ -40,6 +40,7 @@ export class CacheService {
|
|||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.network = network;
|
||||
this.resetBlockCache();
|
||||
this.txCache = {};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { StateService } from './state.service';
|
|||
export class SeoService {
|
||||
network = '';
|
||||
baseTitle = 'mempool';
|
||||
baseDescription = 'Explore the full Bitcoin ecosystem with The Mempool Open Source Project™.';
|
||||
baseDescription = 'Explore the full Bitcoin ecosystem® with The Mempool Open Source Project®.';
|
||||
|
||||
canonicalLink: HTMLElement = document.getElementById('canonical');
|
||||
|
||||
|
|
|
@ -150,6 +150,8 @@ export class StateService {
|
|||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
activeGoggles$: BehaviorSubject<string[]> = new BehaviorSubject([]);
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<p class="explore-tagline-mobile">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||
<div class="selector">
|
||||
|
@ -32,7 +32,7 @@
|
|||
</a>
|
||||
<p class="explore-tagline-desktop">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.">
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more.">
|
||||
|
||||
<meta property="og:image" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more." />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more." />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="https://bisq.markets/">
|
||||
<meta name="twitter:creator" content="@bisq_network">
|
||||
<meta name="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more." />
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Bisq market prices, trading activity, and more." />
|
||||
<meta name="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta name="twitter:domain" content="bisq.markets">
|
||||
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Liquid transactions & assets, get network info, and more.">
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Liquid transactions & assets, get network info, and more.">
|
||||
<meta property="og:image" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1000" />
|
||||
<meta property="og:image:height" content="500" />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Liquid transactions & assets, get network info, and more." />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Liquid transactions & assets, get network info, and more." />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@mempool">
|
||||
<meta name="twitter:creator" content="@mempool">
|
||||
<meta name="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Liquid transactions & assets, get network info, and more." />
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Liquid transactions & assets, get network info, and more." />
|
||||
<meta name="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta name="twitter:domain" content="liquid.network">
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1661,6 +1661,10 @@
|
|||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html</context>
|
||||
<context context-type="linenumber">6</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">accelerator.acceleration-fees</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
|
||||
|
@ -1679,25 +1683,6 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">acceleration.total-bid-boost</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="6c453b11fd7bd159ae30bc381f367bc736d86909" datatype="html">
|
||||
<source>Acceleration Fees</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.html</context>
|
||||
<context context-type="linenumber">6</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/graphs/graphs.component.html</context>
|
||||
<context context-type="linenumber">19</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4793828002882320882" datatype="html">
|
||||
<source>At block: <x id="PH" equiv-text="data[0].data[2]"/></source>
|
||||
<context-group purpose="location">
|
||||
|
@ -1777,8 +1762,8 @@
|
|||
<note priority="1" from="description">BTC</note>
|
||||
<note priority="1" from="meaning">shared.btc</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="4e0fbac5ba55cf78f1accbaf9c871fb23b4b67d9" datatype="html">
|
||||
<source>Success rate</source>
|
||||
<trans-unit id="599dec71fe5c264d05012c7f64080d6347c1dc49" datatype="html">
|
||||
<source>Success Rate</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
|
@ -1941,12 +1926,16 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">accelerations.no-accelerations</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="8adc22d4ccfd987ce3e2c1c86d0ccae17d281328" datatype="html">
|
||||
<source>Active accelerations</source>
|
||||
<trans-unit id="e51c45c636401f8bb3bd8cfd1ed5a3c9810c5fa8" datatype="html">
|
||||
<source>Active Accelerations</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html</context>
|
||||
<context context-type="linenumber">10</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html</context>
|
||||
<context context-type="linenumber">72</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">accelerator.pending-accelerations</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="41a9456b7e195dfc4df3d67b09940bda160882af" datatype="html">
|
||||
|
@ -1965,14 +1954,6 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">mining.144-blocks</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="e51c45c636401f8bb3bd8cfd1ed5a3c9810c5fa8" datatype="html">
|
||||
<source>Active Accelerations</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.html</context>
|
||||
<context context-type="linenumber">72</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">dashboard.recent-accelerations</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="f0ae1220633178276128371f3965fb53d63581d4" datatype="html">
|
||||
<source>Recent Accelerations</source>
|
||||
<context-group purpose="location">
|
||||
|
@ -2020,8 +2001,8 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">accelerator.average-max-bid</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="62be8da2e6a219a43d83a1887e55dc0ae1be155b" datatype="html">
|
||||
<source>Total vsize</source>
|
||||
<trans-unit id="16fedee43f919b6a0992f32aeec5d6938e8d6b76" datatype="html">
|
||||
<source>Total Vsize</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/pending-stats/pending-stats.component.html</context>
|
||||
<context context-type="linenumber">20</context>
|
||||
|
@ -2563,6 +2544,22 @@
|
|||
<context context-type="linenumber">73</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6c453b11fd7bd159ae30bc381f367bc736d86909" datatype="html">
|
||||
<source>Block Fees</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.html</context>
|
||||
<context context-type="linenumber">6</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-fees-graph/block-fees-graph.component.ts</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/graphs/graphs.component.html</context>
|
||||
<context context-type="linenumber">19</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">mining.block-fees</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="meta.description.bitcoin.graphs.block-fees" datatype="html">
|
||||
<source>See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.</source>
|
||||
<context-group purpose="location">
|
||||
|
@ -4268,13 +4265,13 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">master-page.graphs</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="2efef6dfa1c2d2d8fa05b337eccf3e0006af1e94" datatype="html">
|
||||
<source>Acceleration Dashboard</source>
|
||||
<trans-unit id="6b867dc61c6a92f3229f1950f9f2d414790cce95" datatype="html">
|
||||
<source>Accelerator Dashboard</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
|
||||
<context context-type="linenumber">56</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">master-page.acceleration-dashboard</note>
|
||||
<note priority="1" from="description">master-page.accelerator-dashboard</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="142e923d3b04186ac6ba23387265d22a2fa404e0" datatype="html">
|
||||
<source>Lightning Explorer</source>
|
||||
|
@ -5547,11 +5544,11 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">show-diagram</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="9ad256cfb48e88f5bc56243641c992d53461f482" datatype="html">
|
||||
<source>Adjusted vsize <x id="START_LINK" ctype="x-a" equiv-text="<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">"/><x id="START_TAG_FA_ICON" ctype="x-fa_icon" equiv-text="<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true">"/><x id="CLOSE_TAG_FA_ICON" ctype="x-fa_icon"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
|
||||
<trans-unit id="a8a4dd861f790141e19f773153cf42b5d0b0e6b6" datatype="html">
|
||||
<source>Adjusted vsize</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
|
||||
<context context-type="linenumber">302,306</context>
|
||||
<context context-type="linenumber">302</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Transaction Adjusted VSize</note>
|
||||
<note priority="1" from="meaning">transaction.adjusted-vsize</note>
|
||||
|
@ -5564,11 +5561,11 @@
|
|||
</context-group>
|
||||
<note priority="1" from="description">transaction.locktime</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="c93f5659ea1b4a8c59a8e4710cbcdb62b37206b0" datatype="html">
|
||||
<source>Sigops <x id="START_LINK" ctype="x-a" equiv-text="<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">"/><x id="START_TAG_FA_ICON" ctype="x-fa_icon" equiv-text="<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true">"/><x id="CLOSE_TAG_FA_ICON" ctype="x-fa_icon"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/></source>
|
||||
<trans-unit id="3dd65e8fa7035988a691aadcb583862c2a9e336a" datatype="html">
|
||||
<source>Sigops</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
|
||||
<context context-type="linenumber">328,332</context>
|
||||
<context context-type="linenumber">328</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Transaction Sigops</note>
|
||||
<note priority="1" from="meaning">transaction.sigops</note>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1194,4 +1194,5 @@ app-global-footer {
|
|||
|
||||
.info-link fa-icon {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ var PATH;
|
|||
if (process.argv[2]) {
|
||||
PATH = process.argv[2];
|
||||
PATH += PATH.endsWith("/") ? "" : "/"
|
||||
PATH = path.normalize(PATH);
|
||||
PATH = path.resolve(path.normalize(PATH));
|
||||
console.log(`[sync-assets] using PATH ${PATH}`);
|
||||
if (!fs.existsSync(PATH)){
|
||||
console.log(`${LOG_TAG} ${PATH} does not exist, creating`);
|
||||
|
@ -110,7 +110,7 @@ function downloadMiningPoolLogos$() {
|
|||
}
|
||||
let downloadedCount = 0;
|
||||
for (const poolLogo of poolLogos) {
|
||||
const filePath = PATH + `mining-pools/${poolLogo.name}`;
|
||||
const filePath = `${PATH}/mining-pools/${poolLogo.name}`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const localHash = getLocalHash(filePath);
|
||||
if (verbose) {
|
||||
|
@ -124,7 +124,7 @@ function downloadMiningPoolLogos$() {
|
|||
}
|
||||
} else {
|
||||
console.log(`${LOG_TAG} ${poolLogo.name} is missing, downloading...`);
|
||||
const miningPoolsDir = PATH + `mining-pools/`;
|
||||
const miningPoolsDir = `${PATH}/mining-pools/`;
|
||||
if (!fs.existsSync(miningPoolsDir)){
|
||||
fs.mkdirSync(miningPoolsDir, { recursive: true });
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ function downloadPromoVideoSubtiles$() {
|
|||
}
|
||||
let downloadedCount = 0;
|
||||
for (const language of videoLanguages) {
|
||||
const filePath = PATH + `promo-video/${language.name}`;
|
||||
const filePath = `${PATH}/promo-video/${language.name}`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
if (verbose) {
|
||||
console.log(`${LOG_TAG} ${language.name} remote promo video hash ${language.sha}`);
|
||||
|
@ -193,7 +193,7 @@ function downloadPromoVideoSubtiles$() {
|
|||
}
|
||||
} else {
|
||||
console.log(`${LOG_TAG} ${language.name} is missing, downloading`);
|
||||
const promoVideosDir = PATH + `promo-video/`;
|
||||
const promoVideosDir = `${PATH}/promo-video/`;
|
||||
if (!fs.existsSync(promoVideosDir)){
|
||||
fs.mkdirSync(promoVideosDir, { recursive: true });
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ function downloadPromoVideo$() {
|
|||
if (item.name !== 'promo.mp4') {
|
||||
continue;
|
||||
}
|
||||
const filePath = PATH + `promo-video/mempool-promo.mp4`;
|
||||
const filePath = `${PATH}/promo-video/mempool-promo.mp4`;
|
||||
if (fs.existsSync(filePath)) {
|
||||
const localHash = getLocalHash(filePath);
|
||||
|
||||
|
@ -288,16 +288,16 @@ if (configContent.BASE_MODULE && configContent.BASE_MODULE === 'liquid') {
|
|||
const testnetAssetsMinimalJsonUrl = 'https://raw.githubusercontent.com/Blockstream/asset_registry_testnet_db/master/index.minimal.json';
|
||||
|
||||
console.log(`${LOG_TAG} Downloading assets`);
|
||||
download(PATH + 'assets.json', assetsJsonUrl);
|
||||
download(`${PATH}/assets.json`, assetsJsonUrl);
|
||||
|
||||
console.log(`${LOG_TAG} Downloading assets minimal`);
|
||||
download(PATH + 'assets.minimal.json', assetsMinimalJsonUrl);
|
||||
download(`${PATH}/assets.minimal.json`, assetsMinimalJsonUrl);
|
||||
|
||||
console.log(`${LOG_TAG} Downloading testnet assets`);
|
||||
download(PATH + 'assets-testnet.json', testnetAssetsJsonUrl);
|
||||
download(`${PATH}/assets-testnet.json`, testnetAssetsJsonUrl);
|
||||
|
||||
console.log(`${LOG_TAG} Downloading testnet assets minimal`);
|
||||
download(PATH + 'assets-testnet.minimal.json', testnetAssetsMinimalJsonUrl);
|
||||
download(`${PATH}/assets-testnet.minimal.json`, testnetAssetsMinimalJsonUrl);
|
||||
} else {
|
||||
if (verbose) {
|
||||
console.log(`${LOG_TAG} BASE_MODULE is not set to Liquid (${configContent.BASE_MODULE}), skipping downloading assets`);
|
||||
|
|
|
@ -343,7 +343,7 @@ class Server {
|
|||
<meta charset="utf-8">
|
||||
<title>${ogTitle}</title>
|
||||
<link rel="canonical" href="${canonical}" />
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem with mempool.space™"/>
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem with mempool.space®"/>
|
||||
<meta property="og:image" content="${ogImageUrl}"/>
|
||||
<meta property="og:image:type" content="image/png"/>
|
||||
<meta property="og:image:width" content="${matchedRoute.render ? 1200 : 1000}"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue