Compare commits
7 Commits
hunicus/30
...
mononaut/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1037b50851 | ||
|
|
a3c82e44d9 | ||
|
|
78a0059d01 | ||
|
|
172d4f9dfc | ||
|
|
97ecc7b90f | ||
|
|
381d2f0b1f | ||
|
|
38a79249a3 |
2
LICENSE
2
LICENSE
@@ -13,7 +13,7 @@ the terms of (at your option) either:
|
||||
proxy statement published on <https://mempool.space/about>.
|
||||
|
||||
However, this copyright license does not include an implied right or license to
|
||||
use our trademarks: The Mempool Open Source Project®, mempool.space™, the
|
||||
use our trademarks: The Mempool Open Source Project™, mempool.space™, the
|
||||
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
|
||||
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
|
||||
trademarks or trademarks of Mempool Space K.K in Japan, the United States,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# The Mempool Open Source Project® [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||
# The Mempool Open Source Project™ [](https://dashboard.cypress.io/projects/ry4br7/runs)
|
||||
|
||||
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"POLL_RATE_MS": 2000,
|
||||
"CACHE_DIR": "./cache",
|
||||
"CACHE_ENABLED": true,
|
||||
"CLEAR_PROTECTION_MINUTES": 20,
|
||||
"RECOMMENDED_FEE_PERCENTILE": 50,
|
||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||
|
||||
@@ -181,12 +181,12 @@ describe('Mempool Backend Config', () => {
|
||||
// We have a few cases where we can't follow the pattern
|
||||
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
|
||||
console.log('skipping check for MEMPOOL_HTTP_PORT');
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case 'object': {
|
||||
if (Array.isArray(value)) {
|
||||
continue;
|
||||
return;
|
||||
} else {
|
||||
parseJson(value, key);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class Audit {
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
|
||||
const isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
let displacedWeight = 0;
|
||||
@@ -36,9 +36,8 @@ class Audit {
|
||||
// look for transactions that were expected in the template, but missing from the mined block
|
||||
for (const txid of projectedBlocks[0].transactionIds) {
|
||||
if (!inBlock[txid]) {
|
||||
// allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block
|
||||
if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) {
|
||||
rbf.push(txid);
|
||||
if (rbfCache.isFullRbf(txid)) {
|
||||
fullrbf.push(txid);
|
||||
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
|
||||
// tx is recent, may have reached the miner too late for inclusion
|
||||
fresh.push(txid);
|
||||
@@ -99,8 +98,8 @@ class Audit {
|
||||
if (inTemplate[tx.txid]) {
|
||||
matches.push(tx.txid);
|
||||
} else {
|
||||
if (rbfCache.has(tx.txid)) {
|
||||
rbf.push(tx.txid);
|
||||
if (rbfCache.isFullRbf(tx.txid)) {
|
||||
fullrbf.push(tx.txid);
|
||||
} else if (!isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
@@ -148,7 +147,7 @@ class Audit {
|
||||
added,
|
||||
fresh,
|
||||
sigop: [],
|
||||
fullrbf: rbf,
|
||||
fullrbf,
|
||||
score,
|
||||
similarity,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
|
||||
import mempool from '../mempool';
|
||||
import feeApi from '../fee-api';
|
||||
import mempoolBlocks from '../mempool-blocks';
|
||||
import bitcoinApi from './bitcoin-api-factory';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
|
||||
import { Common } from '../common';
|
||||
import backendInfo from '../backend-info';
|
||||
import transactionUtils from '../transaction-utils';
|
||||
@@ -483,7 +483,7 @@ class BitcoinRoutes {
|
||||
returnBlocks.push(localBlock);
|
||||
nextHash = localBlock.previousblockhash;
|
||||
} else {
|
||||
const block = await bitcoinApi.$getBlock(nextHash);
|
||||
const block = await bitcoinCoreApi.$getBlock(nextHash);
|
||||
returnBlocks.push(block);
|
||||
nextHash = block.previousblockhash;
|
||||
}
|
||||
@@ -576,7 +576,7 @@ class BitcoinRoutes {
|
||||
}
|
||||
|
||||
try {
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
|
||||
const addressData = await bitcoinApi.$getScriptHash(req.params.address);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
@@ -597,7 +597,7 @@ class BitcoinRoutes {
|
||||
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||
lastTxId = req.query.after_txid;
|
||||
}
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
|
||||
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.address, lastTxId);
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||
|
||||
@@ -107,16 +107,11 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip expensive lookups while mempool has priority
|
||||
if (onlyCoinbase) {
|
||||
try {
|
||||
const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
|
||||
if (coinbase && coinbase.vin[0].is_coinbase) {
|
||||
return [coinbase];
|
||||
} else {
|
||||
const msg = `Expected a coinbase tx, but the backend API returned something else`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
|
||||
return [coinbase];
|
||||
} catch (e) {
|
||||
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||
logger.err(msg);
|
||||
@@ -141,17 +136,17 @@ class Blocks {
|
||||
|
||||
// Fetch remaining txs individually
|
||||
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
|
||||
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
|
||||
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||
}
|
||||
try {
|
||||
const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
|
||||
transactionMap[txid] = tx;
|
||||
totalFound++;
|
||||
} catch (e) {
|
||||
const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
if (!transactionMap[txid]) {
|
||||
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
|
||||
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
|
||||
}
|
||||
try {
|
||||
const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData);
|
||||
transactionMap[txid] = tx;
|
||||
totalFound++;
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,24 +154,8 @@ class Blocks {
|
||||
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
|
||||
}
|
||||
|
||||
// Require the first transaction to be a coinbase
|
||||
const coinbase = transactionMap[txIds[0]];
|
||||
if (!coinbase || !coinbase.vin[0].is_coinbase) {
|
||||
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Require all transactions to be present
|
||||
// (we should have thrown an error already if a tx request failed)
|
||||
if (txIds.some(txid => !transactionMap[txid])) {
|
||||
const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
|
||||
logger.err(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
// Return list of transactions, preserving block order
|
||||
return txIds.map(txid => transactionMap[txid]);
|
||||
return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,8 +398,8 @@ class Blocks {
|
||||
let newlyIndexed = 0;
|
||||
let totalIndexed = indexedBlockSummariesHashesArray.length;
|
||||
let indexedThisRun = 0;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of indexedBlocks) {
|
||||
if (indexedBlockSummariesHashes[block.hash] === true) {
|
||||
@@ -428,24 +407,17 @@ class Blocks {
|
||||
}
|
||||
|
||||
// Logging
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
|
||||
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||
} else {
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||
}
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
|
||||
|
||||
// Logging
|
||||
indexedThisRun++;
|
||||
@@ -484,18 +456,18 @@ class Blocks {
|
||||
// Logging
|
||||
let count = 0;
|
||||
let countThisRun = 0;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
for (const height of unindexedBlockHeights) {
|
||||
// Logging
|
||||
const hash = await bitcoinApi.$getBlockHash(height);
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = countThisRun / elapsedSeconds;
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = (countThisRun / elapsedSeconds);
|
||||
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
||||
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||
timer = Date.now() / 1000;
|
||||
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
countThisRun = 0;
|
||||
}
|
||||
|
||||
@@ -574,8 +546,8 @@ class Blocks {
|
||||
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
|
||||
let indexedThisRun = 0;
|
||||
let newlyIndexed = 0;
|
||||
const startedAt = Date.now() / 1000;
|
||||
let timer = Date.now() / 1000;
|
||||
const startedAt = new Date().getTime() / 1000;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
while (currentBlockHeight >= lastBlockToIndex) {
|
||||
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
|
||||
@@ -595,18 +567,18 @@ class Blocks {
|
||||
}
|
||||
++indexedThisRun;
|
||||
++totalIndexed;
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
|
||||
timer = Date.now() / 1000;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||
}
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -663,7 +635,7 @@ class Blocks {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
this.currentDifficulty = block.difficulty;
|
||||
@@ -671,7 +643,7 @@ class Blocks {
|
||||
if (blockHeightTip >= 2016) {
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
||||
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
@@ -697,14 +669,14 @@ class Blocks {
|
||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
|
||||
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
// fill in missing transaction fee data from verboseBlock
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
|
||||
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
@@ -869,7 +841,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
const blockHash = await bitcoinApi.$getBlockHash(height);
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -881,7 +853,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
|
||||
@@ -906,7 +878,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
// Bitcoin network, add our custom data on top
|
||||
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
|
||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
|
||||
if (block.stale) {
|
||||
return await this.$indexStaleBlock(hash);
|
||||
} else {
|
||||
@@ -949,15 +921,10 @@ class Blocks {
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(hash, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
height = block.height;
|
||||
}
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
height = block.height;
|
||||
}
|
||||
if (height == null) {
|
||||
const block = await bitcoinApi.$getBlock(hash);
|
||||
@@ -1080,17 +1047,8 @@ class Blocks {
|
||||
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
if (cleanBlock.fee_amt_percentiles === null) {
|
||||
|
||||
let summary;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
summary = this.summarizeBlock(block);
|
||||
}
|
||||
|
||||
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
|
||||
const summary = this.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
|
||||
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
|
||||
}
|
||||
@@ -1150,29 +1108,19 @@ class Blocks {
|
||||
return this.currentBlockHeight;
|
||||
}
|
||||
|
||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
|
||||
let transactions = txs;
|
||||
if (!transactions) {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
}
|
||||
if (!transactions) {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
transactions = block.tx.map(tx => {
|
||||
tx.fee *= 100_000_000;
|
||||
return tx;
|
||||
});
|
||||
}
|
||||
}
|
||||
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
const transactions = block.tx.map(tx => {
|
||||
tx.fee *= 100_000_000;
|
||||
return tx;
|
||||
});
|
||||
|
||||
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
|
||||
const summary = Common.calculateCpfp(height, transactions);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
|
||||
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {
|
||||
|
||||
@@ -179,7 +179,6 @@ class DiskCache {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const start = Date.now();
|
||||
let data: any = {};
|
||||
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
if (cacheData) {
|
||||
@@ -221,8 +220,6 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
|
||||
|
||||
await memPool.$setMempool(data.mempool);
|
||||
if (!this.ignoreBlocksCache) {
|
||||
blocks.setBlocks(data.blocks);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import config from '../config';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
@@ -182,7 +182,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
|
||||
public async $updateMempool(transactions: string[]): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -258,7 +258,7 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
|
||||
if (Date.now() - intervalTimer > 5_000) {
|
||||
if (this.inSync) {
|
||||
// Break and restart mempool loop if we spend too much time processing
|
||||
// new transactions that may lead to falling behind on block height
|
||||
@@ -270,7 +270,7 @@ class Mempool {
|
||||
if (Math.floor(progress) < 100) {
|
||||
loadingIndicators.setProgress('mempool', progress);
|
||||
}
|
||||
intervalTimer = Date.now();
|
||||
intervalTimer = Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ class Mempool {
|
||||
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
||||
setTimeout(() => {
|
||||
this.mempoolProtection = 2;
|
||||
logger.warn('Mempool clear protection ended, normal operation resumed.');
|
||||
logger.warn('Mempool clear protection resumed.');
|
||||
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
|
||||
import config from '../../config';
|
||||
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
|
||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||
import database from '../../database';
|
||||
|
||||
@@ -201,7 +201,7 @@ class Mining {
|
||||
try {
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||
|
||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||
@@ -312,7 +312,7 @@ class Mining {
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
try {
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.timestamp * 1000;
|
||||
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||
const lastMidnight = this.getDateMidnight(new Date());
|
||||
@@ -421,9 +421,8 @@ class Mining {
|
||||
}
|
||||
|
||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
|
||||
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
|
||||
let currentDifficulty = genesisBlock.difficulty;
|
||||
let currentBits = genesisBlock.bits;
|
||||
let totalIndexed = 0;
|
||||
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
||||
@@ -437,18 +436,17 @@ class Mining {
|
||||
|
||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||
currentBits = oldestConsecutiveBlock.bits;
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
}
|
||||
|
||||
let totalBlockChecked = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.bits !== currentBits) {
|
||||
if (block.difficulty !== currentDifficulty) {
|
||||
if (indexedHeights[block.height] === true) { // Already indexed
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -466,7 +464,6 @@ class Mining {
|
||||
totalIndexed++;
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
currentBits = block.bits;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,24 +146,6 @@ class RbfCache {
|
||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||
}
|
||||
|
||||
public has(txId: string): boolean {
|
||||
return this.txs.has(txId);
|
||||
}
|
||||
|
||||
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
|
||||
const tree = this.getRbfTree(txId);
|
||||
if (!tree) {
|
||||
return false;
|
||||
}
|
||||
const txs = this.getTransactionsInTree(tree);
|
||||
for (const tx of txs) {
|
||||
if (predicate(tx)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
return this.replacedBy.get(txId);
|
||||
}
|
||||
@@ -360,14 +342,14 @@ class RbfCache {
|
||||
|
||||
public async load({ txs, trees, expiring }): Promise<void> {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.key, txEntry.value);
|
||||
this.txs.set(txEntry[0], txEntry[1]);
|
||||
});
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
}
|
||||
expiring.forEach(expiringEntry => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
|
||||
if (this.txs.has(expiringEntry[0])) {
|
||||
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
|
||||
}
|
||||
});
|
||||
this.cleanup();
|
||||
|
||||
@@ -36,6 +36,13 @@ class RedisCache {
|
||||
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||
});
|
||||
this.$ensureConnected();
|
||||
this.client.exists('mempool:0').then((mempoolExists) => {
|
||||
if (!mempoolExists) {
|
||||
for (let i = 0; i < 16; i++) {
|
||||
this.client.json.set(`mempool:${i.toString(16)}`, '$', {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +65,7 @@ class RedisCache {
|
||||
async $updateBlocks(blocks: BlockExtended[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('blocks', JSON.stringify(blocks));
|
||||
logger.debug(`Saved latest blocks to Redis cache`);
|
||||
await this.client.json.set('blocks', '$', blocks);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -68,10 +74,9 @@ class RedisCache {
|
||||
async $updateBlockSummaries(summaries: BlockSummary[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set('block-summaries', JSON.stringify(summaries));
|
||||
logger.debug(`Saved latest block summaries to Redis cache`);
|
||||
await this.client.json.set('block-summaries', '$', summaries);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,20 +90,17 @@ class RedisCache {
|
||||
async $flushTransactions() {
|
||||
const success = await this.$addTransactions(this.cacheQueue);
|
||||
if (success) {
|
||||
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
logger.info(`Flushed ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
this.cacheQueue = [];
|
||||
} else {
|
||||
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
logger.err(`Failed to flush ${this.cacheQueue.length} transactions to Redis cache`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
||||
if (!newTransactions.length) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const msetData = newTransactions.map(tx => {
|
||||
await Promise.all(newTransactions.map(tx => {
|
||||
const minified: any = { ...tx };
|
||||
delete minified.hex;
|
||||
for (const vin of minified.vin) {
|
||||
@@ -109,9 +111,8 @@ class RedisCache {
|
||||
for (const vout of minified.vout) {
|
||||
delete vout.scriptpubkey_asm;
|
||||
}
|
||||
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
|
||||
});
|
||||
await this.client.MSET(msetData);
|
||||
return this.client.json.set(`mempool:${tx.txid.slice(0,1)}`, tx.txid, minified);
|
||||
}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
@@ -122,11 +123,9 @@ class RedisCache {
|
||||
async $removeTransactions(transactions: string[]) {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
|
||||
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
|
||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||
}
|
||||
await Promise.all(transactions.map(txid => {
|
||||
return this.client.json.del(`mempool:${txid.slice(0,1)}`, txid);
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -135,7 +134,7 @@ class RedisCache {
|
||||
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
|
||||
await this.client.json.set(`rbf:${type}:${txid}`, '$', value);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -144,7 +143,7 @@ class RedisCache {
|
||||
async $removeRbfEntry(type: string, txid: string): Promise<void> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
await this.client.unlink(`rbf:${type}:${txid}`);
|
||||
await this.client.del(`rbf:${type}:${txid}`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
@@ -153,8 +152,7 @@ class RedisCache {
|
||||
async $getBlocks(): Promise<BlockExtended[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('blocks');
|
||||
return JSON.parse(json);
|
||||
return this.client.json.get('blocks');
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
@@ -164,8 +162,7 @@ class RedisCache {
|
||||
async $getBlockSummaries(): Promise<BlockSummary[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const json = await this.client.get('block-summaries');
|
||||
return JSON.parse(json);
|
||||
return this.client.json.get('block-summaries');
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
@@ -174,14 +171,16 @@ class RedisCache {
|
||||
|
||||
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
|
||||
const start = Date.now();
|
||||
const mempool = {};
|
||||
let mempool = {};
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
|
||||
for (const tx of mempoolList) {
|
||||
mempool[tx.key] = tx.value;
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const shard = await this.client.json.get(`mempool:${i.toString(16)}`);
|
||||
logger.info(`Loaded ${Object.keys(shard).length} transactions from redis cache ${i.toString(16)}`);
|
||||
mempool = Object.assign(mempool, shard);
|
||||
}
|
||||
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
|
||||
logger.info(`Total ${Object.keys(mempool).length} transactions loaded from redis cache `);
|
||||
logger.info(`Loaded redis cache in ${Date.now() - start} ms`);
|
||||
return mempool || {};
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
@@ -192,8 +191,17 @@ class RedisCache {
|
||||
async $getRbfEntries(type: string): Promise<any[]> {
|
||||
try {
|
||||
await this.$ensureConnected();
|
||||
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
|
||||
return rbfEntries;
|
||||
const keys = await this.client.keys(`rbf:${type}:*`);
|
||||
const promises: Promise<MempoolTransactionExtended[]>[] = [];
|
||||
for (let i = 0; i < keys.length; i += 10000) {
|
||||
const keySlice = keys.slice(i, i + 10000);
|
||||
if (!keySlice.length) {
|
||||
continue;
|
||||
}
|
||||
promises.push(this.client.json.mGet(keySlice, '$').then(chunk => chunk?.length ? chunk.flat().map((v, i) => [keySlice[i].slice(`rbf:${type}:`.length), v]) : [] ));
|
||||
}
|
||||
const entries = await Promise.all(promises);
|
||||
return entries.flat();
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||
return [];
|
||||
@@ -219,7 +227,7 @@ class RedisCache {
|
||||
await memPool.$setMempool(loadedMempool);
|
||||
await rbfCache.load({
|
||||
txs: rbfTxs,
|
||||
trees: rbfTrees.map(loadedTree => loadedTree.value),
|
||||
trees: rbfTrees.map(loadedTree => loadedTree[1]),
|
||||
expiring: rbfExpirations,
|
||||
});
|
||||
}
|
||||
@@ -240,37 +248,6 @@ class RedisCache {
|
||||
}
|
||||
}
|
||||
|
||||
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
|
||||
logger.info(`loading Redis entries for ${pattern}`);
|
||||
let keys: string[] = [];
|
||||
const result: { key: string, value: T }[] = [];
|
||||
const patternLength = pattern.length - 1;
|
||||
let count = 0;
|
||||
const processValues = async (keys): Promise<void> => {
|
||||
const values = await this.client.MGET(keys);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (values[i]) {
|
||||
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
|
||||
count++;
|
||||
}
|
||||
}
|
||||
logger.info(`loaded ${count} entries from Redis cache`);
|
||||
};
|
||||
for await (const key of this.client.scanIterator({
|
||||
MATCH: pattern,
|
||||
COUNT: 100
|
||||
})) {
|
||||
keys.push(key);
|
||||
if (keys.length >= 10000) {
|
||||
await processValues(keys);
|
||||
keys = [];
|
||||
}
|
||||
}
|
||||
if (keys.length) {
|
||||
await processValues(keys);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default new RedisCache();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
import { Common } from './common';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import logger from '../logger';
|
||||
|
||||
class TransactionUtils {
|
||||
constructor() { }
|
||||
@@ -23,23 +22,6 @@ class TransactionUtils {
|
||||
};
|
||||
}
|
||||
|
||||
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
|
||||
// Propagates any error from the retry request.
|
||||
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||
try {
|
||||
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
// retry direct from Core if first request failed
|
||||
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId
|
||||
* @param addPrevouts
|
||||
@@ -49,7 +31,7 @@ class TransactionUtils {
|
||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
|
||||
let transaction: IEsploraApi.Transaction;
|
||||
if (forceCore === true) {
|
||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
||||
} else {
|
||||
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||
}
|
||||
|
||||
@@ -183,25 +183,15 @@ 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})$/
|
||||
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})$/
|
||||
.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'] = null;
|
||||
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
|
||||
} else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
|
||||
} else {
|
||||
client['track-address'] = matchedAddress;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
client['track-address'] = matchedAddress;
|
||||
} else {
|
||||
client['track-address'] = null;
|
||||
client['track-scriptpubkey'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,44 +546,6 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
@@ -869,33 +821,6 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-scriptpubkey']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = JSON.stringify(foundTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
|
||||
@@ -188,15 +188,14 @@ class Server {
|
||||
}
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, pollRate);
|
||||
await memPool.$updateMempool(newMempool);
|
||||
}
|
||||
indexer.$run();
|
||||
|
||||
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
|
||||
const elapsed = Date.now() - start;
|
||||
const remainingTime = Math.max(0, pollRate - elapsed);
|
||||
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
|
||||
this.backendRetryCount = 0;
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
@@ -13,7 +12,6 @@ import config from '../config';
|
||||
import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
@@ -541,7 +539,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getBlocksDifficulty(): Promise<object[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
|
||||
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -850,7 +848,7 @@ class BlocksRepository {
|
||||
*/
|
||||
public async $getOldestConsecutiveBlock(): Promise<any> {
|
||||
try {
|
||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, bits FROM blocks ORDER BY height DESC`);
|
||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
|
||||
for (let i = 0; i < rows.length - 1; ++i) {
|
||||
if (rows[i].height - rows[i + 1].height > 1) {
|
||||
return rows[i];
|
||||
@@ -1038,17 +1036,8 @@ class BlocksRepository {
|
||||
{
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
if (extras.feePercentiles === null) {
|
||||
|
||||
let summary;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
} else {
|
||||
// Call Core RPC
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
summary = blocks.summarizeBlock(block);
|
||||
}
|
||||
|
||||
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
|
||||
const summary = blocks.summarizeBlock(block);
|
||||
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
|
||||
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||
|
||||
Signed: Czino
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
|
||||
Signed: bguillaumat
|
||||
@@ -1,5 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||
I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
|
||||
And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
|
||||
|
||||
Signed: fiatjaf
|
||||
@@ -1,3 +0,0 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
|
||||
|
||||
Signed: rishkwal
|
||||
@@ -8,7 +8,6 @@
|
||||
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
|
||||
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
|
||||
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
|
||||
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
|
||||
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
|
||||
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
|
||||
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
|
||||
@@ -134,9 +133,5 @@
|
||||
"AUDIT": __REPLICATION_AUDIT__,
|
||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||
"SERVERS": __REPLICATION_SERVERS__
|
||||
},
|
||||
"REDIS": {
|
||||
"ENABLED": __REDIS_ENABLED__,
|
||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
|
||||
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
|
||||
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
|
||||
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
|
||||
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
|
||||
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
|
||||
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
|
||||
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
|
||||
@@ -137,9 +136,6 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||
|
||||
# REDIS
|
||||
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||
|
||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||
|
||||
@@ -151,7 +147,6 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
|
||||
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
|
||||
@@ -170,7 +165,7 @@ sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-co
|
||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_GBT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||
@@ -267,8 +262,4 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||
|
||||
# REDIS
|
||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||
|
||||
node /backend/package/index.js
|
||||
|
||||
@@ -18,7 +18,7 @@ fi
|
||||
|
||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
|
||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
|
||||
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
|
||||
|
||||
@@ -411,7 +411,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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>.
|
||||
|
||||
@@ -64,12 +64,12 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = null;
|
||||
this.addressInfo = null;
|
||||
this.addressString = params.get('id') || '';
|
||||
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(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
return (this.addressString.match(/[a-f0-9]{130}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
|
||||
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addressInfo = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
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(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
@@ -84,7 +84,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => (
|
||||
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
this.addressString.match(/[a-f0-9]{130}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
@@ -118,7 +118,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingAddress = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
@@ -166,8 +166,31 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.stateService.mempoolTransactions$
|
||||
.subscribe(tx => {
|
||||
this.addTransaction(tx);
|
||||
.subscribe((transaction) => {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin.prevout.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
@@ -177,47 +200,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
tx.status = transaction.status;
|
||||
this.transactions = this.transactions.slice();
|
||||
this.audioService.playSound('magic');
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (playSound) {
|
||||
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
return;
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'rbf':
|
||||
case 'fullrbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isRbf = {};
|
||||
const isFullRbf = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -363,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isRbf[txid] = true;
|
||||
isFullRbf[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
@@ -381,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
@@ -398,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
}
|
||||
}
|
||||
if (this.widget) {
|
||||
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||
blocks[1][0].extras.pool.slug + '.svg';
|
||||
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
}
|
||||
acc.unshift(blocks[1][0]);
|
||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../..//services/state.service';
|
||||
@@ -61,7 +61,6 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
@@ -190,15 +189,9 @@ export class DifficultyComponent implements OnInit {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
@HostListener('pointerdown', ['$event'])
|
||||
onPointerDown(event) {
|
||||
this.onPointerMove(event);
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onHover(event, rect): void {
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
blockSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
chainTipSubscription: Subscription;
|
||||
keySubscription: Subscription;
|
||||
isTabHiddenSubscription: Subscription;
|
||||
network = '';
|
||||
now = new Date().getTime();
|
||||
timeOffset = 0;
|
||||
@@ -118,7 +116,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.calculateTransactionPosition();
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
this.stateService.isLoadingMempool$
|
||||
@@ -226,7 +224,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
|
||||
this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||
this.stateService.keyNavigation$.subscribe((event) => {
|
||||
if (this.markIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -237,12 +235,13 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
const blocks = this.stateService.blocksSubject$.getValue();
|
||||
for (const block of (blocks || [])) {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
}
|
||||
this.stateService.blocks$
|
||||
.pipe(map((blocks) => blocks[0]))
|
||||
.subscribe((block) => {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (event.key === nextKey) {
|
||||
if (this.mempoolBlocks[this.markIndex + 1]) {
|
||||
@@ -266,8 +265,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.keySubscription.unsubscribe();
|
||||
this.isTabHiddenSubscription.unsubscribe();
|
||||
clearTimeout(this.resetTransitionTimeout);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining-dashboard',
|
||||
@@ -10,12 +8,10 @@ import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
styleUrls: ['./mining-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
export class MiningDashboardComponent implements OnInit {
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private router: Router
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||
}
|
||||
@@ -23,15 +19,4 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
this.router.events.subscribe((e: NavigationStart) => {
|
||||
if (e.type === EventType.NavigationStart) {
|
||||
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
|
||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
||||
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
if (logoSrc === this.lastImgSrc) {
|
||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
|
||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||
|
||||
return Object.assign({
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||
}, poolStats);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project™ on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@@ -47,8 +47,6 @@ export class SearchFormComponent implements OnInit {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private router: Router,
|
||||
@@ -57,26 +55,11 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
}
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
this.stateService.searchFocus$.subscribe(() => {
|
||||
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
|
||||
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
|
||||
} else if (this.searchInput) {
|
||||
this.searchInput.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: ['', Validators.required],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h5>The Mempool Open Source Project ™</h5>
|
||||
<h6>Updated: July 19, 2021</h6>
|
||||
<br>
|
||||
|
||||
@@ -304,7 +304,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 Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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 Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address))
|
||||
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, 132) === this.address))
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
@@ -56,8 +56,8 @@
|
||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, -2)]" title="{{ vin.prevout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, 132)]" title="{{ vin.prevout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||
</a></span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
@@ -184,7 +184,7 @@
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address))
|
||||
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, 132) === this.address))
|
||||
}">
|
||||
<td class="address-cell">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
@@ -192,8 +192,8 @@
|
||||
</a>
|
||||
<ng-template #pubkey_type>
|
||||
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, 132)]" title="{{ vout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
@@ -31,7 +31,7 @@ interface MempoolStatsData {
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
featuredAssets$: Observable<any>;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
@@ -57,10 +57,6 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private seoService: SeoService
|
||||
) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
@@ -159,7 +155,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6));
|
||||
@@ -171,11 +167,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
switchMap(() => this.apiService.list2HStatistics$()),
|
||||
switchMap((mempoolStats) => {
|
||||
return merge(
|
||||
this.stateService.live2Chart$
|
||||
@@ -190,14 +182,10 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
);
|
||||
}),
|
||||
map((mempoolStats) => {
|
||||
if (mempoolStats) {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
@@ -110,7 +110,6 @@ export interface PoolInfo {
|
||||
regexes: string; // JSON array
|
||||
addresses: string; // JSON array
|
||||
emptyBlocks: number;
|
||||
slug: string;
|
||||
}
|
||||
export interface PoolStat {
|
||||
pool: PoolInfo;
|
||||
@@ -175,7 +174,7 @@ export interface TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export interface TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from '../../interfaces/node-api.interface';
|
||||
@@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
styleUrls: ['./lightning-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
export class LightningDashboardComponent implements OnInit {
|
||||
statistics$: Observable<any>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
@@ -30,7 +30,4 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
height: calc(100vh - 250px);
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -67,8 +67,7 @@ export class ElectrsApiService {
|
||||
}
|
||||
|
||||
getPubKeyAddress$(pubkey: string): Observable<Address> {
|
||||
const scriptpubkey = (pubkey.length === 130 ? '41' : '21') + pubkey + 'ac';
|
||||
return this.getScriptHash$(scriptpubkey).pipe(
|
||||
return this.getScriptHash$('41' + pubkey + 'ac').pipe(
|
||||
switchMap((scripthash: ScriptHash) => {
|
||||
return of({
|
||||
...scripthash,
|
||||
|
||||
@@ -96,7 +96,7 @@ export class MiningService {
|
||||
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
||||
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
||||
logo: `/resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
|
||||
...poolStat
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||
|
||||
export interface MarkBlockState {
|
||||
blockHeight?: number;
|
||||
@@ -140,8 +139,6 @@ export class StateService {
|
||||
fiatCurrency$: BehaviorSubject<string>;
|
||||
rateUnits$: BehaviorSubject<string>;
|
||||
|
||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -359,10 +356,4 @@ export class StateService {
|
||||
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
||||
this.blocksSubject$.next(this.blocks);
|
||||
}
|
||||
|
||||
focusSearchInputDesktop() {
|
||||
if (!hasTouchScreen()) {
|
||||
this.searchFocus$.next(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class WebsocketService {
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
if (this.stateService.connectionState$.value !== 2) {
|
||||
if (this.stateService.connectionState$.value === 1) {
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row main">
|
||||
<div class="offset-lg-1 col-lg-4 col align-self-center branding">
|
||||
<div class="main-logo">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
</div>
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<p><ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></p>
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
@@ -20,16 +17,17 @@
|
||||
<a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a>
|
||||
<p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p>
|
||||
</ng-template>
|
||||
<p class="cta-secondary"><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace && env.LIGHTNING" class="cta-secondary"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/about' | relativeUrl]">About The Mempool Open Source Project™</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
@@ -40,22 +38,24 @@
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs ›</a></p>
|
||||
</div>
|
||||
<!--<div class="col-lg-4 links">
|
||||
<p class="category">Connect</p>
|
||||
<p><a href="https://github.com/mempool" target="_blank">GitHub</a></p>
|
||||
<p><a href="https://twitter.com/mempool" target="_blank">Twitter</a></p>
|
||||
<p><a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank">Nostr</a></p>
|
||||
<p><a href="https://youtube.com/@mempool" target="_blank">YouTube</a></p>
|
||||
<p><a href="https://bitcointv.com/c/mempool/videos" target="_blank">BitcoinTV</a></p>
|
||||
<p><a href="https://mempool.chat" target="_blank">Matrix</a></p>
|
||||
</div>-->
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<p class="category">Networks</p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')">Liquid Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links" *ngIf="!(officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED)">
|
||||
<p class="category">Tools</p>
|
||||
<p><a [routerLink]="['/clock/mempool/0']">Clock (Mempool)</a></p>
|
||||
<p><a [routerLink]="['/clock/mined/0']">Clock (Mined)</a></p>
|
||||
<p><a [routerLink]="['/tools/calculator']">BTC/Fiat Converter</a></p>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">More Networks</p>
|
||||
<p *ngIf="currentNetwork !== '' && currentNetwork !== 'mainnet'"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'testnet'"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'signet'"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'liquid' && currentNetwork !== 'liquidtestnet'"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'bisq'"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Legal</p>
|
||||
|
||||
@@ -21,10 +21,6 @@ footer .row.main .branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .row.main .branding > p {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
|
||||
footer .row.main .branding .btn {
|
||||
display: inline-block;
|
||||
color: #fff !important;
|
||||
@@ -93,11 +89,6 @@ footer .row.version p a {
|
||||
color: #09a3ba;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
max-width: 220px;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
footer .row.main .links.outer {
|
||||
|
||||
@@ -309,28 +309,3 @@ export function takeWhile(input: any[], predicate: CollectionPredicate) {
|
||||
return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) =>
|
||||
!predicate(item, index, collection));
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
export function hasTouchScreen(): boolean {
|
||||
let hasTouchScreen = false;
|
||||
if ('maxTouchPoints' in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ('msMaxTouchPoints' in navigator) {
|
||||
// @ts-ignore
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
const mQ = matchMedia?.('(pointer:coarse)');
|
||||
if (mQ?.media === '(pointer:coarse)') {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ('orientation' in window) {
|
||||
hasTouchScreen = true; // deprecated, but good fallback
|
||||
} else {
|
||||
// @ts-ignore - Only as a last resort, fall back to user agent sniffing
|
||||
const UA = navigator.userAgent;
|
||||
hasTouchScreen =
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
|
||||
}
|
||||
}
|
||||
return hasTouchScreen;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem.">
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
|
||||
<meta property="og:image" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="https://bisq.markets/">
|
||||
<meta property="twitter:creator" content="@bisq_network">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta property="twitter:domain" content="bisq.markets">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem.">
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
<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" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="@mempool">
|
||||
<meta property="twitter:creator" content="@mempool">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta property="twitter:domain" content="liquid.network">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem." />
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
|
||||
<meta property="og:image" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1000" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="@mempool">
|
||||
<meta property="twitter:creator" content="@mempool">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||
<meta property="twitter:domain" content="mempool.space">
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 308 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 726 KiB |
24
production/electrs-start-liquid
Executable file
24
production/electrs-start-liquid
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env zsh
|
||||
cd "${HOME}/electrs"
|
||||
#source "${HOME}/.cargo/env"
|
||||
#export PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
|
||||
until false
|
||||
do
|
||||
cargo run \
|
||||
--release \
|
||||
--features liquid \
|
||||
--bin electrs \
|
||||
-- \
|
||||
-vvv \
|
||||
--asset-db-path "${HOME}/asset_registry_db" \
|
||||
--address-search \
|
||||
--cors '*' \
|
||||
--db-dir __ELECTRS_DATA_ROOT__ \
|
||||
--network liquid \
|
||||
--daemon-dir "${HOME}" \
|
||||
--http-socket-file '/elements/socket/esplora-liquid-mainnet' \
|
||||
--cookie '__ELEMENTS_RPC_USER__:__ELEMENTS_RPC_PASS__' \
|
||||
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt"
|
||||
sleep 1
|
||||
done
|
||||
24
production/electrs-start-liquidtestnet
Executable file
24
production/electrs-start-liquidtestnet
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/local/bin/zsh
|
||||
cd "${HOME}/electrs"
|
||||
#source "${HOME}/.cargo/env"
|
||||
#export PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
|
||||
until false
|
||||
do
|
||||
cargo run \
|
||||
--release \
|
||||
--features liquid \
|
||||
--bin electrs \
|
||||
-- \
|
||||
-vv \
|
||||
--asset-db-path "${HOME}/asset_registry_testnet_db" \
|
||||
--address-search \
|
||||
--cors '*' \
|
||||
--db-dir __ELECTRS_DATA_ROOT__ \
|
||||
--network liquidtestnet \
|
||||
--daemon-dir "${HOME}" \
|
||||
--http-socket-file '/elements/socket/esplora-liquid-testnet' \
|
||||
--cookie '__ELEMENTS_RPC_USER__:__ELEMENTS_RPC_PASS__' \
|
||||
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt"
|
||||
sleep 1
|
||||
done
|
||||
22
production/electrs-start-mainnet
Executable file
22
production/electrs-start-mainnet
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env zsh
|
||||
cd "${HOME}/electrs"
|
||||
#source "${HOME}/.cargo/env"
|
||||
#export PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
|
||||
until false
|
||||
do
|
||||
cargo run \
|
||||
--release \
|
||||
--bin electrs \
|
||||
-- \
|
||||
-vvvv \
|
||||
--address-search \
|
||||
--cors '*' \
|
||||
--db-dir __ELECTRS_DATA_ROOT__ \
|
||||
--daemon-dir "${HOME}" \
|
||||
--http-socket-file '/bitcoin/socket/esplora-bitcoin-mainnet' \
|
||||
--cookie '__BITCOIN_RPC_USER__:__BITCOIN_RPC_PASS__' \
|
||||
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt"
|
||||
|
||||
sleep 3
|
||||
done
|
||||
23
production/electrs-start-signet
Executable file
23
production/electrs-start-signet
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env zsh
|
||||
cd "${HOME}/electrs"
|
||||
#source "${HOME}/.cargo/env"
|
||||
#export PATH="${HOME}/.cargo/bin:${PATH}"
|
||||
|
||||
until false
|
||||
do
|
||||
cargo run \
|
||||
--release \
|
||||
--bin electrs \
|
||||
-- \
|
||||
-vv \
|
||||
--network signet \
|
||||
--address-search \
|
||||
--cors '*' \
|
||||
--db-dir __ELECTRS_DATA_ROOT__ \
|
||||
--daemon-rpc-addr '127.0.0.1:38332' \
|
||||
--daemon-dir "${HOME}" \
|
||||
--http-socket-file '/bitcoin/socket/esplora-bitcoin-signet' \
|
||||
--cookie '__BITCOIN_RPC_USER__:__BITCOIN_RPC_PASS__' \
|
||||
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt"
|
||||
sleep 1
|
||||
done
|
||||
23
production/electrs-start-testnet
Executable file
23
production/electrs-start-testnet
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env zsh
|
||||
cd "${HOME}/electrs"
|
||||
#source $HOME/.cargo/env
|
||||
#export PATH=$HOME/.cargo/bin:$PATH
|
||||
|
||||
until false
|
||||
do
|
||||
cargo run \
|
||||
--release \
|
||||
--bin electrs \
|
||||
-- \
|
||||
-vvvv \
|
||||
--network testnet \
|
||||
--address-search \
|
||||
--cors '*' \
|
||||
--db-dir __ELECTRS_DATA_ROOT__ \
|
||||
--daemon-dir "${HOME}" \
|
||||
--http-socket-file '/bitcoin/socket/esplora-bitcoin-testnet' \
|
||||
--cookie '__BITCOIN_RPC_USER__:__BITCOIN_RPC_PASS__' \
|
||||
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt"
|
||||
|
||||
sleep 3
|
||||
done
|
||||
@@ -1603,8 +1603,15 @@ fi
|
||||
########################################
|
||||
# Electrs instance for Bitcoin Mainnet #
|
||||
########################################
|
||||
|
||||
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
echo "[*] Installing Bitcoin Mainnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
fi
|
||||
|
||||
########################################
|
||||
@@ -1612,7 +1619,13 @@ fi
|
||||
########################################
|
||||
|
||||
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
echo "[*] Installing Bitcoin Testnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
fi
|
||||
|
||||
#######################################
|
||||
@@ -1620,7 +1633,13 @@ fi
|
||||
#######################################
|
||||
|
||||
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
echo "[*] Installing Bitcoin Signet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
fi
|
||||
|
||||
########################################
|
||||
@@ -1628,12 +1647,21 @@ fi
|
||||
########################################
|
||||
|
||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Elements Liquid electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquid" "${ELEMENTS_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Installing Elements crontab"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
|
||||
fi
|
||||
|
||||
################################################
|
||||
@@ -1662,6 +1690,7 @@ fi
|
||||
echo "[*] Installing crontabs"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
|
||||
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
|
||||
;;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"SERVER": {
|
||||
"HOST": "https://mempool.tk7.mempool.space",
|
||||
"HOST": "https://mempool.fra.mempool.space",
|
||||
"HTTP_PORT": 8001
|
||||
},
|
||||
"MEMPOOL": {
|
||||
|
||||
@@ -174,7 +174,7 @@ class Server {
|
||||
const { lang, path } = parseLanguageUrl(rawPath);
|
||||
const matchedRoute = matchRoute(this.network, path);
|
||||
let ogImageUrl = config.SERVER.HOST + (matchedRoute.staticImg || matchedRoute.fallbackImg);
|
||||
let ogTitle = 'The Mempool Open Source Project®';
|
||||
let ogTitle = 'The Mempool Open Source Project™';
|
||||
|
||||
if (matchedRoute.render) {
|
||||
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
||||
@@ -187,7 +187,7 @@ class Server {
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${ogTitle}</title>
|
||||
<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}"/>
|
||||
|
||||
Reference in New Issue
Block a user