Compare commits

...

44 Commits

Author SHA1 Message Date
natsoni
b6b3e52436 Shades of green / red on DA blocks 2024-09-02 16:18:23 +02:00
natsoni
d04e5128ba Add endpoint for difficulty adjustment by height 2024-09-02 16:16:53 +02:00
softsimon
e3c3f31ddb Merge pull request #5494 from vostrnad/zero-multisig
Allow OP_0 in multisig scripts
2024-09-01 10:49:27 +04:00
wiz
70d1f52268 Merge pull request #5489 from mempool/mononaut/the-v3-standard 2024-09-01 02:02:21 +09:00
Vojtěch Strnad
e44f30d7a7 Allow OP_0 in multisig scripts 2024-08-31 14:31:55 +02:00
Mononaut
099d84a395 New standardness rules for v3 & anchor outputs, with activation height logic 2024-08-30 23:16:33 +00:00
Mononaut
12285465d9 Add support for anchor output type 2024-08-30 21:39:22 +00:00
softsimon
3bea10ea35 Merge pull request #5484 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.14.0
Bump cypress from 13.13.0 to 13.14.0 in /frontend
2024-08-28 07:00:55 +02:00
dependabot[bot]
1ea45e9e96 Bump cypress from 13.13.0 to 13.14.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.0 to 13.14.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.13.0...v13.14.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 02:59:35 +00:00
softsimon
8c2d0e1d6c Merge pull request #5445 from mempool/mononaut/persistent-goggles
Persist mempool block visualization between pages
2024-08-26 22:14:17 +02:00
softsimon
009fba3dd5 Merge branch 'master' into mononaut/persistent-goggles 2024-08-26 22:05:16 +02:00
softsimon
a0fc4861d4 Merge pull request #5460 from mempool/mononaut/v1-audit-improvements
v1 audit improvements
2024-08-26 21:38:37 +02:00
softsimon
62085581dd Merge branch 'master' into mononaut/v1-audit-improvements 2024-08-26 18:45:00 +02:00
softsimon
05efa8c300 Merge pull request #5468 from mempool/dependabot/npm_and_yarn/frontend/elliptic-6.5.7
Bump elliptic from 6.5.4 to 6.5.7 in /frontend
2024-08-26 17:59:21 +02:00
softsimon
eee99a6407 Merge pull request #5477 from mempool/mononaut/readme-node-version
[docs] update READMEs to newer node version
2024-08-26 17:58:52 +02:00
Mononaut
98cee4a6cd [docs] update READMEs to newer node version 2024-08-26 15:36:53 +00:00
dependabot[bot]
0302999806 Bump elliptic from 6.5.4 to 6.5.7 in /frontend
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 14:54:51 +00:00
softsimon
1876d67e74 Merge pull request #5467 from mempool/dependabot/npm_and_yarn/backend/axios-1.7.4
Bump axios from 1.7.2 to 1.7.4 in /backend
2024-08-26 16:53:40 +02:00
softsimon
c0bb75e5b1 Merge pull request #5475 from mempool/dependabot/npm_and_yarn/frontend/tslib-2.7.0
Bump tslib from 2.6.2 to 2.7.0 in /frontend
2024-08-26 16:53:26 +02:00
dependabot[bot]
4059a902a1 Bump tslib from 2.6.2 to 2.7.0 in /frontend
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.2 to 2.7.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.6.2...v2.7.0)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:46 +00:00
dependabot[bot]
4cc19a7235 Bump axios from 1.7.2 to 1.7.4 in /backend
Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 12:42:42 +00:00
wiz
c874d642c5 Bump version to 3.1.0-dev 2024-08-26 21:41:26 +09:00
wiz
f0af1703da Release v3.0.0 2024-08-24 18:35:41 +09:00
softsimon
5452d7f524 pull from transifex 2024-08-20 09:32:56 +02:00
wiz
ff9e2456b9 ops: Tweak build script to support tags 2024-08-20 15:22:16 +09:00
wiz
4e581347c8 Bump version to v3.0.0-rc1 2024-08-20 12:06:11 +09:00
wiz
820777236e Merge pull request #5465 from mempool/orangesurf/update-logos-2024-08-19
Add logo images and references to logos
2024-08-20 12:01:51 +09:00
wiz
beeb5eb08c Merge pull request #5466 from mempool/mononaut/fix-about-layout
Fix about page layout
2024-08-20 10:50:04 +09:00
Mononaut
b78aca0282 Fix about page layout 2024-08-19 19:46:22 +00:00
orangesurf
9572f2d554 Add logo images and references to logos 2024-08-19 20:13:49 +02:00
softsimon
ef13596b59 Merge pull request #5449 from mempool/mononaut/non-acc-effective-fee
fix mined acceleration detection logic on tx pages
2024-08-19 17:10:46 +02:00
wiz
80da024bbb Add hr locale to angular.json 2024-08-19 14:49:36 +09:00
natsoni
f75f85f914 Hide fee delta on accelerated tx mined by participating pool with 0 bid boost 2024-08-18 19:43:38 +02:00
natsoni
b3ac107b0b clear feeDelta if a tx is mined by non-participating pool 2024-08-18 18:35:30 +02:00
softsimon
f8cedaa7a3 Merge pull request #5462 from mempool/natsoni/fix-console-error
Fix accelerated arrow not appearing
2024-08-18 15:47:02 +02:00
natsoni
72bb92dd8b Merge branch 'master' into mononaut/non-acc-effective-fee 2024-08-18 14:27:48 +02:00
natsoni
e3c4e219f3 Fix accelerated arrow not appearing 2024-08-18 14:15:56 +02:00
wiz
aa3fa4478a Merge pull request #5458 from mempool/mononaut/pool-pie-colors
update pool pie chart color scheme
2024-08-18 12:15:27 +09:00
Mononaut
c9171224e1 DB migration to fix bad v1 audits 2024-08-18 02:21:12 +00:00
Mononaut
248cef7718 Improve prioritized transaction detection algorithm 2024-08-18 02:21:05 +00:00
Mononaut
26c03eee88 update pool pie chart color scheme 2024-08-14 14:21:47 +00:00
Mononaut
a31729b8b8 fix feeDelta display logic 2024-08-10 21:56:11 +00:00
Mononaut
79e494150c fix mined acceleration detection logic on tx pages 2024-08-09 14:44:51 +00:00
Mononaut
104c7f4285 Persist mempool block visualization between pages 2024-08-08 13:12:31 +00:00
64 changed files with 813 additions and 190 deletions

12
LICENSE
View File

@@ -1,5 +1,5 @@
The Mempool Open Source Project®
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
@@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
of Mempool Space K.K in Japan, the United States, and/or other countries.
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
registered trademarks or trademarks of Mempool Space K.K in Japan,
the United States, and/or other countries.
See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>.

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer_
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._

View File

@@ -1,18 +1,19 @@
{
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",
@@ -2277,9 +2278,9 @@
}
},
"node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -9438,9 +9439,9 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
},
"axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -42,7 +42,7 @@
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "~1.7.2",
"axios": "~1.7.4",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.19.2",

View File

@@ -2,6 +2,7 @@ import config from '../config';
import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache';
import transactionUtils from './transaction-utils';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
@@ -15,7 +16,8 @@ class Audit {
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
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 accelerated: string[] = []; // prioritized by the mempool accelerator
@@ -133,23 +135,7 @@ class Audit {
totalWeight += tx.weight;
}
// identify "prioritized" transactions
let lastEffectiveRate = 0;
// Iterate over the mined template from bottom to top (excluding the coinbase)
// Transactions should appear in ascending order of mining priority.
for (let i = transactions.length - 1; i > 0; i--) {
const blockTx = transactions[i];
// If a tx has a lower in-band effective fee rate than the previous tx,
// it must have been prioritized out-of-band (in order to have a higher mining priority)
// so exclude from the analysis.
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
prioritized.push(blockTx.txid);
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
} else if (!isAccelerated[blockTx.txid]) {
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
}
}
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);

View File

@@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return'
};

View File

@@ -219,10 +219,10 @@ class Blocks {
};
}
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
return {
id: hash,
transactions: Common.classifyTransactions(transactions),
transactions: Common.classifyTransactions(transactions, height),
};
}
@@ -616,7 +616,7 @@ class Blocks {
// add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@@ -653,7 +653,7 @@ class Blocks {
}
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx;
@@ -912,7 +912,7 @@ class Blocks {
}
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) {
@@ -1169,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -1188,7 +1188,7 @@ class Blocks {
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs);
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
summaryVersion = 1;
} else {
// Call Core RPC
@@ -1324,7 +1324,7 @@ class Blocks {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC

View File

@@ -10,7 +10,6 @@ import logger from '../logger';
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -200,10 +199,13 @@ export class Common {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
static isNonStandard(tx: TransactionExtended): boolean {
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
// version
if (tx.version > TX_MAX_STANDARD_VERSION) {
if (this.isNonStandardVersion(tx, height)) {
return true;
}
@@ -250,6 +252,8 @@ export class Common {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@@ -335,6 +339,49 @@ export class Common {
return false;
}
// Individual versioned standardness rules
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
if (
height != null
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;
@@ -415,7 +462,7 @@ export class Common {
return flags;
}
static getTransactionFlags(tx: TransactionExtended): number {
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -548,7 +595,7 @@ export class Common {
if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey;
}
// fast but bad heuristic to detect possible coinjoins
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@@ -564,17 +611,17 @@ export class Common {
flags |= TransactionFlags.batch_payout;
}
if (this.isNonStandard(tx)) {
if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard;
}
return Number(flags);
}
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
flags = Common.getTransactionFlags(tx, height);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
@@ -585,8 +632,8 @@ export class Common {
};
}
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
return txs.map(Common.classifyTransaction);
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(tx => Common.classifyTransaction(tx, height));
}
static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 81;
private static currentVersion = 82;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -700,6 +700,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81);
}
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82);
}
}
/**
@@ -1314,6 +1319,28 @@ class DatabaseMigration {
logger.warn(`Failed to migrate cpfp transaction data`);
}
}
private async $fixBadV1AuditBlocks(): Promise<void> {
const badBlocks = [
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
];
for (const hash of badBlocks) {
try {
await this.$executeQuery(`
UPDATE blocks_audits
SET prioritized_txs = '[]'
WHERE hash = '${hash}'
`, true);
} catch (e) {
continue;
}
}
}
}
export default new DatabaseMigration();

View File

@@ -30,6 +30,7 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustment/:height', this.$getDifficultyAdjustment)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
@@ -297,6 +298,18 @@ class MiningRoutes {
}
}
private async $getDifficultyAdjustment(req: Request, res: Response) {
try {
const adjustment = await DifficultyAdjustmentsRepository.$getAdjustmentAtHeight(parseInt(req.params.height, 10));
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(adjustment);
} catch (e) {
res.status(e instanceof Error && e.message === 'not found' ? 204 : 500).send(e instanceof Error ? e.message : e);
}
}
private async $getRewardStats(req: Request, res: Response) {
try {
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));

View File

@@ -338,6 +338,87 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript];
}
// calculate the most parsimonious set of prioritizations given a list of block transactions
// (i.e. the most likely prioritizations and deprioritizations)
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: any[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = X[0].rate;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}
}
export default new TransactionUtils();

View File

@@ -1106,7 +1106,7 @@ class BlocksRepository {
let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
summaryVersion = 1;
} else {
// Call Core RPC

View File

@@ -88,6 +88,22 @@ class DifficultyAdjustmentsRepository {
}
}
public async $getAdjustmentAtHeight(height: number): Promise<IndexedDifficultyAdjustment> {
try {
if (isNaN(height)) {
throw new Error(`argument must be a number`);
}
const [rows] = await DB.query(`SELECT * FROM difficulty_adjustments WHERE height = ?`, [height]);
if (!rows[0]) {
throw new Error(`not found`);
}
return rows[0] as IndexedDifficultyAdjustment;
} catch (e: any) {
logger.err(`Cannot get difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
throw e;
}
}
public async $getAdjustmentsHeights(): Promise<number[]> {
try {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);

View File

@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opN) {
return;
}
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
if (!opM) {
return;
}
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Install project dependencies and run the frontend server:
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Make sure to use Node.js 16.10 and npm 7._
_Make sure to use Node.js 20.x and npm 9.x or newer._
Build the frontend:

View File

@@ -54,6 +54,10 @@
"translation": "src/locale/messages.fr.xlf",
"baseHref": "/fr/"
},
"hr": {
"translation": "src/locale/messages.hr.xlf",
"baseHref": "/hr/"
},
"ja": {
"translation": "src/locale/messages.ja.xlf",
"baseHref": "/ja/"

View File

@@ -750,7 +750,7 @@
},
"backendInfo": {
"hostname": "node205.tk7.mempool.space",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"gitCommit": "abbc8a134",
"lightning": false
},

View File

@@ -1,12 +1,12 @@
{
"name": "mempool-frontend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-frontend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",
@@ -32,6 +32,7 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.14.0",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.23.0",
@@ -42,7 +43,7 @@
"rxjs": "~7.8.1",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.7.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -62,7 +63,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.13.0",
"cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -699,6 +700,11 @@
"node": ">=10"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@angular-devkit/build-webpack": {
"version": "0.1703.1",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz",
@@ -8040,13 +8046,13 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@cypress/request": "^3.0.0",
"@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -8805,9 +8811,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
},
"node_modules/elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -16925,9 +16931,9 @@
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"node_modules/tuf-js": {
"version": "2.2.0",
@@ -18849,6 +18855,11 @@
"requires": {
"lru-cache": "^6.0.0"
}
},
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}
}
},
@@ -24127,12 +24138,12 @@
"peer": true
},
"cypress": {
"version": "13.13.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
"version": "13.14.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",
"@cypress/request": "^3.0.1",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -24723,9 +24734,9 @@
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -30763,9 +30774,9 @@
}
},
"tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"tuf-js": {
"version": "2.2.0",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-frontend",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -95,7 +95,7 @@
"esbuild": "^0.23.0",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"tslib": "~2.7.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.13.0",
"cypress": "^13.14.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
return;
}
const opN = ops.pop();
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
}
}
const opM = ops.pop();
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);

View File

@@ -53,7 +53,7 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76" class="image">
<defs>
<style>
.d {
@@ -125,7 +125,9 @@
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -150,7 +152,7 @@
<span>Bull Bitcoin</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
@@ -435,7 +437,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -13,8 +13,6 @@
.image.not-rounded {
border-radius: 0;
width: 60px;
height: 60px;
}
.intro {
@@ -158,9 +156,8 @@
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
height: auto;
margin: 20px 29px 20px;
}
}
}

View File

@@ -67,13 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
// Find the first pool with at least 1% of the total network hashrate
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem(
pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);

View File

@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
}
handleVin() {
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
if (address?.scripts.size) {
const script = address?.scripts.values().next().value;
if (script.template?.label) {

View File

@@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
// initialize the scene without any entry transition
setup(transactions: TransactionStripped[]): void {
setup(transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
@@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.filtersAvailable = filtersAvailable;
if (this.scene) {
this.clearUpdateQueue();
this.scene.setup(transactions);
this.scene.setup(transactions, sort);
this.readyNextFrame = true;
this.start();
this.updateSearchHighlight();

View File

@@ -88,16 +88,19 @@ export default class BlockScene {
}
// set up the scene with an initial set of transactions, without any transition animation
setup(txs: TransactionStripped[]) {
setup(txs: TransactionStripped[], sort: boolean = false) {
// clean up any old transactions
Object.values(this.txs).forEach(tx => {
tx.destroy();
delete this.txs[tx.txid];
});
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => {
const txView = new TxView(tx, this);
this.txs[tx.txid] = txView;
let txViews = txs.map(tx => new TxView(tx, this));
if (sort) {
txViews = txViews.sort(feeRateDescending);
}
txViews.forEach(txView => {
this.txs[txView.txid] = txView;
this.place(txView);
this.saveGridToScreenPosition(txView);
this.applyTxUpdate(txView, {

View File

@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
scene?: BlockScene;

View File

@@ -142,6 +142,10 @@ export function defaultColorFunction(
return auditColors.added_prioritized;
case 'prioritized':
return auditColors.prioritized;
case 'added_deprioritized':
return auditColors.added_prioritized;
case 'deprioritized':
return auditColors.prioritized;
case 'selected':
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
case 'accelerated':

View File

@@ -79,6 +79,11 @@
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container>
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
<ng-container *ngSwitchCase="'added_deprioritized'">
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
</ng-container>
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@@ -78,6 +78,25 @@
</ng-template>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
<td i18n="mining.difficulty-adjustment">Adjustment</td>
<td>
<ng-container *ngIf="cacheService.daCache[block.height]?.adjustment > 0; else loadingAdjustment">
<div [style.color]="cacheService.daCache[block.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[block.height].adjustment < 1 ? 'var(--red)' : '')">
@if (cacheService.daCache[block.height].adjustment > 1) {
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
} @else if (cacheService.daCache[block.height].adjustment < 1) {
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
}
{{ (cacheService.daCache[block.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
</ng-container>
<ng-template #loadingAdjustment>
<span class="skeleton-loader" style="max-width: 60px"></span>
</ng-template>
</td>
</tr>
</ng-container>
<ng-template #skeletonRows>
<tr>
@@ -193,6 +212,10 @@
</span>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
<td i18n="block.difficulty">Difficulty</td>
<td>{{ block.difficulty | amountShortener: 2 }}</td>
</tr>
</ng-container>
<ng-template #loadingRest>
<tr>

View File

@@ -280,3 +280,11 @@ h1 {
top: -1px;
margin-right: 2px;
}
.retarget-sign {
margin-right: -3px;
&.up {
position: relative;
top: 2px;
}
}

View File

@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
@Component({
selector: 'app-block',
@@ -98,7 +99,7 @@ export class BlockComponent implements OnInit, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService,
private priceService: PriceService,
private cacheService: CacheService,
public cacheService: CacheService,
private servicesApiService: ServicesApiServices,
private cd: ChangeDetectorRef,
private preloadService: PreloadService,
@@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isUnseen = {};
const isAdded = {};
const isPrioritized = {};
const isDeprioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
@@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
this.numUnexpected = 0;
if (blockAudit?.template) {
// augment with locally calculated *de*prioritized transactions if possible
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
// but if the local calculation produces returns unexpected results, don't use it
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
for (const tx of prioritized) {
if (!isPrioritized[tx] && !isAccelerated[tx]) {
useLocalDeprioritized = false;
break;
}
}
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
@@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs || []) {
for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true;
}
if (useLocalDeprioritized) {
for (const txid of deprioritized || []) {
isDeprioritized[txid] = true;
}
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
@@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else {
tx.status = 'prioritized';
}
} else if (isDeprioritized[tx.txid]) {
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
tx.status = 'added_deprioritized';
} else {
tx.status = 'deprioritized';
}
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {

View File

@@ -1,10 +1,12 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
import { Observable, Subscription, delay, filter, of, retryWhen, switchMap, take, tap, throwError } from 'rxjs';
import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { Location } from '@angular/common';
import { CacheService } from '../../services/cache.service';
import { ApiService } from '../../services/api.service';
import { colorFromRetarget } from '../../shared/common.utils';
interface BlockchainBlock extends BlockExtended {
placeholder?: boolean;
@@ -77,6 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor(
public stateService: StateService,
public cacheService: CacheService,
public apiService: ApiService,
private cd: ChangeDetectorRef,
private location: Location,
) {
@@ -334,6 +337,31 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
}
isDA(height: number): boolean {
const isDA = height % 2016 === 0 && this.stateService.network === '';
if (isDA && !this.cacheService.daCache[height]?.exact) {
const estimatedAdjustment = this.cacheService.daCache[height]?.adjustment || 0;
this.cacheService.daCache[height] = { adjustment: estimatedAdjustment, exact: true };
this.apiService.getDifficultyAdjustmentByHeight$(height).pipe(
switchMap(da => {
const blocksAvailable = (this.height || this.chainTip) && this.blockStyles[(this.height || this.chainTip) - height];
return blocksAvailable ? of(da) : throwError(() => new Error());
}),
retryWhen(errors =>
errors.pipe(
delay(1000),
take(3)
)
),
tap((da) => {
this.cacheService.daCache[height] = { adjustment: da?.adjustment || 1, exact: true };
this.blockStyles[(this.height || this.chainTip) - height].background = colorFromRetarget(da?.adjustment);
})
).subscribe();
}
return isDA;
}
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
if (!block || block.placeholder) {
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
@@ -349,7 +377,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + this.blockOffset * index + 'px',
background: `repeating-linear-gradient(
background: this.isDA(block.height) ? colorFromRetarget(this.cacheService.daCache[block.height]?.adjustment || 1) :
`repeating-linear-gradient(
var(--secondary),
var(--secondary) ${greenBackgroundHeight}%,
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,

View File

@@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service';
import { formatNumber } from '@angular/common';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-difficulty-adjustments-table',
@@ -27,7 +28,8 @@ export class DifficultyAdjustmentsTable implements OnInit {
constructor(
@Inject(LOCALE_ID) public locale: string,
private apiService: ApiService,
public stateService: StateService
public stateService: StateService,
private cacheService: CacheService,
) {
}
@@ -52,6 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
adjustment[2] / selectedPowerOfTen.divider,
this.locale, `1.${decimals}-${decimals}`) + selectedPowerOfTen.unit
});
this.cacheService.daCache[adjustment[1]] = { adjustment: adjustment[3], exact: true };
}
this.isLoading = false;
return tableData.slice(0, 6);

View File

@@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
lastBlockHeight: number;
blockIndex: number;
isLoading$ = new BehaviorSubject<boolean>(true);
isLoading$ = new BehaviorSubject<boolean>(false);
timeLtrSubscription: Subscription;
timeLtr: boolean;
chainDirection: string = 'right';
@@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
}
this.updateBlock({
block: this.blockIndex,
removed,
changed,
added
@@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
}
this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) {
this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions));
} else {
this.isLoading$.next(true);
}
}
}
@@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
this.isLoading$.next(false);
}
resumeBlock(transactionsStripped: TransactionStripped[]): void {
if (this.blockGraph) {
this.firstLoad = false;
this.blockGraph.setup(transactionsStripped, true);
this.blockIndex = this.index;
this.isLoading$.next(false);
} else {
requestAnimationFrame(() => {
this.resumeBlock(transactionsStripped);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {

View File

@@ -41,6 +41,25 @@
</div>
</td>
</tr>
<tr *ngIf="this.stateService.network === '' && mempoolBlock.height % 2016 === 0">
<td i18n="mining.difficulty-adjustment">Adjustment</td>
<td>
<ng-container *ngIf="cacheService.daCache[mempoolBlock.height]?.adjustment > 0; else loadingAdjustment">
<div [style.color]="cacheService.daCache[mempoolBlock.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[mempoolBlock.height].adjustment < 1 ? 'var(--red)' : '')">
@if (cacheService.daCache[mempoolBlock.height].adjustment > 1) {
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
} @else if (cacheService.daCache[mempoolBlock.height].adjustment < 1) {
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
}
{{ (cacheService.daCache[mempoolBlock.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
</ng-container>
<ng-template #loadingAdjustment>
<span class="skeleton-loader" style="max-width: 60px"></span>
</ng-template>
</td>
</tr>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>

View File

@@ -36,3 +36,11 @@ h1 {
margin: auto;
}
}
.retarget-sign {
margin-right: -3px;
&.up {
position: relative;
top: 2px;
}
}

View File

@@ -9,6 +9,7 @@ import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { WebsocketService } from '../../services/websocket.service';
import { CacheService } from '../../services/cache.service';
@Component({
selector: 'app-mempool-block',
@@ -30,6 +31,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
public cacheService: CacheService,
private cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId: Object,
) {
@@ -71,7 +73,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
})
);
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions)));
this.network$ = this.stateService.networkChanged$;
}

View File

@@ -1,10 +1,10 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { Subscription, Observable, of, combineLatest } from 'rxjs';
import { Subscription, Observable, of, combineLatest, throwError } from 'rxjs';
import { MempoolBlock } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { EtaService } from '../../services/eta.service';
import { Router } from '@angular/router';
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
import { delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
import { feeLevels } from '../../app.constants';
import { specialBlocks } from '../../app.constants';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -12,6 +12,8 @@ import { Location } from '@angular/common';
import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface';
import { animate, style, transition, trigger } from '@angular/animations';
import { ThemeService } from '../../services/theme.service';
import { CacheService } from '../../services/cache.service';
import { colorFromRetarget } from '../../shared/common.utils';
@Component({
selector: 'app-mempool-blocks',
@@ -93,6 +95,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
constructor(
private router: Router,
public stateService: StateService,
public cacheService: CacheService,
private etaService: EtaService,
private themeService: ThemeService,
private cd: ChangeDetectorRef,
@@ -213,7 +216,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition.accelerated) {
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
this.acceleratingArrow = true;
setTimeout(() => {
this.acceleratingArrow = false;
@@ -387,6 +390,37 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i)));
}
isDA(height: number): boolean {
if (this.chainTip === -1) {
return false;
}
const isDA = height % 2016 === 0 && this.stateService.network === '';
if (isDA && !this.cacheService.daCache[height]) {
this.cacheService.daCache[height] = { adjustment: 0 };
this.difficultyAdjustments$.pipe(
filter(da => !!da),
switchMap(da => {
const mempoolBlocksAvailable = this.chainTip && this.mempoolBlockStyles[height - this.chainTip - 1];
return mempoolBlocksAvailable ? of(da) : throwError(() => new Error());
}),
retryWhen(errors =>
errors.pipe(
delay(100),
take(3)
)
),
tap(da => {
const adjustment = parseFloat((1 + da.difficultyChange / 100).toFixed(4));
if (adjustment !== this.cacheService.daCache[height].adjustment) {
this.cacheService.daCache[height].adjustment = adjustment;
this.mempoolBlockStyles[height - this.chainTip - 1].background = colorFromRetarget(adjustment);
}
})
).subscribe();
}
return isDA;
}
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
@@ -410,7 +444,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
'right': this.containerOffset + index * this.blockOffset + 'px',
'background': backgroundGradients.join(',') + ')'
'background': this.isDA(mempoolBlock.height) ? colorFromRetarget(this.cacheService.daCache[mempoolBlock.height]?.adjustment || 1) : backgroundGradients.join(',') + ')'
};
}

View File

@@ -293,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
})
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() {
if (this.tx) {
this.tx.flags = getTransactionFlags(this.tx);
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop;

View File

@@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: July 3, 2024</h6>
<h6>Updated: August 19, 2024</h6>
<br>
<div class="text-left">
@@ -100,11 +100,26 @@
<p>The Mempool Accelerator Logo</p>
<br><br>
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool research Logo</p>
<br><br>
<app-svg-images name="goggles" height="96px"></app-svg-images>
<br><br>
<p>The Mempool Goggles Logo</p>
<br><br>
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool transaction Logo</p>
<br><br>
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool block visualization Logo</p>
<br><br>
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
<br><br>
<p>The mempool Blocks Logo</p>

View File

@@ -606,16 +606,11 @@
@if (!isLoadingTx) {
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo.bidBoost | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + accelerationInfo.bidBoost"></app-fiat></span>
} @else if (tx.feeDelta && !accelerationInfo) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + tx.feeDelta"></app-fiat></span>
} @else {
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span>
}
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
}
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
</td>
</tr>
} @else {

View File

@@ -358,12 +358,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}),
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
if (acceleration.txid === this.txId) {
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
} else {
this.tx.feeDelta = undefined;
}
}
this.waitingForAccelerationInfo = false;
this.setIsAccelerated();
}
@@ -895,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
this.tx.flags = getTransactionFlags(this.tx);
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility();
} else {

View File

@@ -3670,6 +3670,39 @@ export const restApiDocsData = [
}
}
},
{
type: "endpoint",
category: "mining",
httpRequestMethod: "GET",
fragment: "get-difficulty-adjustment-by-height",
title: "GET Difficulty Adjustment",
description: {
default: "<p>Returns difficulty adjustment for the block at the specified <code>:blockHeight</code>. If no adjustment happened at that height, an empty response is returned.</p>"
},
urlString: "/v1/mining/difficulty-adjustment/:blockHeight",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `/api/v1/mining/difficulty-adjustment/%{1}`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: [`756000`],
response: `{
"time": "2022-09-28T02:56:34.000Z",
"height": 756000,
"difficulty": 31360548173144.9,
"adjustment": 0.97863
}`
},
}
}
},
{
type: "endpoint",
category: "mining",

View File

@@ -239,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean;
flags?: number | null;
time?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual';
}

View File

@@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
}
export interface MempoolBlockDelta {
block: number;
added: TransactionStripped[];
removed: string[];
changed: { txid: string, rate: number, flags: number, acc: boolean }[];
}
export interface MempoolBlockState {
block: number;
transactions: TransactionStripped[];
}
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;

View File

@@ -315,6 +315,12 @@ export class ApiService {
);
}
getDifficultyAdjustmentByHeight$(height: number): Observable<any> {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustment/${height}`
);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +

View File

@@ -23,6 +23,7 @@ export class CacheService {
blockLoading: { [height: number]: boolean } = {};
copiesInBlockQueue: { [height: number]: number } = {};
blockPriorities: number[] = [];
daCache: { [height: number]: { adjustment: number, exact?: boolean } } = {};
constructor(
private stateService: StateService,
@@ -128,6 +129,7 @@ export class CacheService {
this.blockLoading = {};
this.copiesInBlockQueue = {};
this.blockPriorities = [];
this.daCache = {};
}
getCachedBlock(height) {

View File

@@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { filter, map, scan, share, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ActiveFilter } from '../shared/filters.utils';
@@ -131,6 +131,7 @@ export class StateService {
latestBlockHeight = -1;
blocks: BlockExtended[] = [];
mempoolSequence: number;
mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} };
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1);
@@ -143,7 +144,7 @@ export class StateService {
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
txConfirmed$ = new Subject<[string, BlockExtended]>();
@@ -231,29 +232,40 @@ export class StateService {
}
});
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => {
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => {
if (isMempoolState(change)) {
const txMap = {};
change.transactions.forEach(tx => {
txMap[tx.txid] = tx;
});
return txMap;
this.mempoolBlockState = {
block: change.block,
transactions: txMap
};
return this.mempoolBlockState;
} else {
change.added.forEach(tx => {
transactions[tx.txid] = tx;
acc.transactions[tx.txid] = tx;
});
change.removed.forEach(txid => {
delete transactions[txid];
delete acc.transactions[txid];
});
change.changed.forEach(tx => {
if (transactions[tx.txid]) {
transactions[tx.txid].rate = tx.rate;
transactions[tx.txid].acc = tx.acc;
if (acc.transactions[tx.txid]) {
acc.transactions[tx.txid].rate = tx.rate;
acc.transactions[tx.txid].acc = tx.acc;
}
});
return transactions;
this.mempoolBlockState = {
block: change.block,
transactions: acc.transactions
};
return this.mempoolBlockState;
}
}, {}));
}, {}),
share()
);
this.liveMempoolBlockTransactions$.subscribe();
// Emits the full list of pending accelerations each time it changes
this.liveAccelerations$ = this.accelerations$.pipe(

View File

@@ -35,6 +35,7 @@ export class WebsocketService {
private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number;
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = '';
private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number;
@@ -203,19 +204,31 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' });
}
startTrackMempoolBlock(block: number, force: boolean = false) {
startTrackMempoolBlock(block: number, force: boolean = false): boolean {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
// skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block;
return true;
}
return false;
}
stopTrackMempoolBlock() {
this.websocketSubject.next({ 'track-mempool-block': -1 });
stopTrackMempoolBlock(): void {
if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
this.isTrackingMempoolBlock = false;
this.trackingMempoolBlock = null;
this.stoppingTrackMempoolBlock = setTimeout(() => {
this.stoppingTrackMempoolBlock = null;
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.trackingMempoolBlock = null;
this.stateService.mempoolBlockState = null;
}, 2000);
}
startTrackRbf(mode: 'all' | 'fullRbf') {
@@ -424,6 +437,7 @@ export class WebsocketService {
if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next({
block: this.trackingMempoolBlock,
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
});
} else if (response['projected-block-transactions'].delta) {
@@ -432,7 +446,7 @@ export class WebsocketService {
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
} else {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
}
}
}

View File

@@ -17,6 +17,7 @@ export type AddressType = 'fee'
| 'v0_p2wsh'
| 'v1_p2tr'
| 'confidential'
| 'anchor'
| 'unknown'
const ADDRESS_PREFIXES = {
@@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
for (const v of vin) {
if (v.prevout?.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}
// and there's nothing more to learn from processing inputs for other types
}
@@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
}
} else if (this.type === 'unknown') {
if (output.scriptpubkey === '51024e73') {
this.type = 'anchor';
}
}
}

View File

@@ -170,8 +170,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
};
}
export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
return {
block,
added: delta.added.map(uncompressTx),
removed: delta.removed,
changed: delta.changed.map(tx => ({
@@ -239,3 +240,25 @@ export function md5(inputString): string {
}
return rh(a)+rh(b)+rh(c)+rh(d);
}
export function colorFromRetarget(da: number): string {
const minDA = 0.95;
const maxDA = 1.05;
const midDA = 1;
const red = { r: 220, g: 53, b: 69 };
const grey = { r: 108, g: 117, b: 125 };
const green = { r: 59, g: 204, b: 73 };
const interpolateColor = (color1, color2, ratio) => {
ratio = Math.min(1, Math.max(0, ratio));
const r = Math.round(color1.r + ratio * (color2.r - color1.r));
const g = Math.round(color1.g + ratio * (color2.g - color1.g));
const b = Math.round(color1.b + ratio * (color2.b - color1.b));
return `rgba(${r}, ${g}, ${b}, 0.7)`;
}
return da <= midDA ?
interpolateColor(red, grey, (da - minDA) / (midDA - minDA)) :
interpolateColor(grey, green, (da - midDA) / (maxDA - midDA));
}

View File

@@ -20,6 +20,9 @@
@case ('multisig') {
<span i18n="address.bare-multisig">bare multisig</span>
}
@case ('anchor') {
<span>anchor</span>
}
@case (null) {
<span>unknown</span>
}

View File

@@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
anchor: () => ({ type: 'anchor', label: 'anchor' }),
};
export class ScriptInfo {
@@ -266,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opN) {
return;
}
if (!opN.startsWith('OP_PUSHNUM_')) {
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
return;
}
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
@@ -286,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
if (!opM) {
return;
}
if (!opM.startsWith('OP_PUSHNUM_')) {
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
return;
}
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);

View File

@@ -1,10 +1,10 @@
import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface';
import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface';
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
import { StateService } from '../services/state.service';
// Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
@@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
*
* returns true early if any standardness rule is violated, otherwise false
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
*
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
* For now, just pull out individual rules into versioned functions where necessary.
*/
export function isNonStandard(tx: Transaction): boolean {
export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
// version
if (tx.version > TX_MAX_STANDARD_VERSION) {
if (isNonStandardVersion(tx, height, network)) {
return true;
}
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
}
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true;
} else if (isNonStandardAnchor(tx, height, network)) {
return true;
}
// TODO: bad-witness-nonstandard
}
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false;
}
// Individual versioned standardness rules
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
let TX_MAX_STANDARD_VERSION = 3;
if (
height != null
&& network != null
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
TX_MAX_STANDARD_VERSION = 2;
}
if (tx.version > TX_MAX_STANDARD_VERSION) {
return true;
}
return false;
}
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 42_000,
'testnet': 2_900_000,
'signet': 211_000,
'': 863_500,
};
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
if (
height != null
&& network != null
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
) {
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
return true;
}
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
@@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
].includes(pubkey);
}
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF)
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout;
}
if (isNonStandard(tx)) {
if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard;
}
@@ -458,4 +508,83 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean):
} else {
return tx.effectiveFeePerVsize;
}
}
export function identifyPrioritizedTransactions(transactions: TransactionStripped[]): { prioritized: string[], deprioritized: string[] } {
// find the longest increasing subsequence of transactions
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
// should be O(n log n)
const X = transactions.slice(1).reverse(); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
if (X.length < 2) {
return { prioritized: [], deprioritized: [] };
}
const N = X.length;
const P: number[] = new Array(N);
const M: number[] = new Array(N + 1);
M[0] = -1; // undefined so can be set to any value
let L = 0;
for (let i = 0; i < N; i++) {
// Binary search for the smallest positive l ≤ L
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
let lo = 1;
let hi = L + 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
if (X[M[mid]].rate > X[i].rate) {
hi = mid;
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
lo = mid + 1;
}
}
// After searching, lo == hi is 1 greater than the
// length of the longest prefix of X[i]
const newL = lo;
// The predecessor of X[i] is the last index of
// the subsequence of length newL-1
P[i] = M[newL - 1];
M[newL] = i;
if (newL > L) {
// If we found a subsequence longer than any we've
// found yet, update L
L = newL;
}
}
// Reconstruct the longest increasing subsequence
// It consists of the values of X at the L indices:
// ..., P[P[M[L]]], P[M[L]], M[L]
const LIS: TransactionStripped[] = new Array(L);
let k = M[L];
for (let j = L - 1; j >= 0; j--) {
LIS[j] = X[k];
k = P[k];
}
const lisMap = new Map<string, number>();
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
const prioritized: string[] = [];
const deprioritized: string[] = [];
let lastRate = 0;
for (const tx of X) {
if (lisMap.has(tx.txid)) {
lastRate = tx.rate;
} else {
if (Math.abs(tx.rate - lastRate) < 0.1) {
// skip if the rate is almost the same as the previous transaction
} else if (tx.rate <= lastRate) {
prioritized.push(tx.txid);
} else {
deprioritized.push(tx.txid);
}
}
}
return { prioritized, deprioritized };
}

View File

@@ -1567,7 +1567,7 @@
</trans-unit>
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
<source>Total Bid Boost</source>
<target>Ukupno povećanje ponude</target>
<target>Total Bid Boost</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
<context context-type="linenumber">11</context>
@@ -1969,7 +1969,7 @@
</trans-unit>
<trans-unit id="841f2a74ae5095e6e37f5749f3cc1851cf36a420" datatype="html">
<source>Avg Max Bid</source>
<target>Prosječna maks. ponuda</target>
<target>Prosj. max ponuda</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/pending-stats/pending-stats.component.html</context>
<context context-type="linenumber">11</context>
@@ -3622,7 +3622,7 @@
</trans-unit>
<trans-unit id="14779b0ce4cbc4d975a35a8fe074426228a324f3" datatype="html">
<source><x id="INTERPOLATION" equiv-text="transactions&lt;/ng-template&gt; &lt;/h2&gt; &lt;ngb-pagination class=&quot;pagination-container float-ri"/> transactions</source>
<target><x id="INTERPOLATION" equiv-text="transactions&lt;/ng-template&gt; &lt;/h2&gt; &lt;ngb-pagination class=&quot;pagination-container float-ri"/> transakcije</target>
<target><x id="INTERPOLATION" equiv-text="transactions&lt;/ng-template&gt; &lt;/h2&gt; &lt;ngb-pagination class=&quot;pagination-container float-ri"/> transakcija</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/block/block-transactions.component.html</context>
<context context-type="linenumber">5</context>
@@ -4046,7 +4046,7 @@
</trans-unit>
<trans-unit id="8a7b4bd44c0ac71b2e72de0398b303257f7d2f54" datatype="html">
<source>Blocks</source>
<target>Blokovi</target>
<target>Blokova</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">4</context>
@@ -4360,7 +4360,7 @@
</trans-unit>
<trans-unit id="23c872b0336e20284724607f2887da39bd8142c3" datatype="html">
<source>Previous fee</source>
<target>Prethodna naknada</target>
<target>Preth. naknada</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
<context context-type="linenumber">107</context>
@@ -4594,7 +4594,7 @@
</trans-unit>
<trans-unit id="1bb6965f8e1bbe40c076528ffd841da86f57f119" datatype="html">
<source><x id="INTERPOLATION" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;blocks&lt;/span&gt;&lt;/ng-template&gt; &lt;ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;"/>blocks<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
<target><x id="INTERPOLATION" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;blocks&lt;/span&gt;&lt;/ng-template&gt; &lt;ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;"/>blokovi<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
<target><x id="INTERPOLATION" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;blocks&lt;/span&gt;&lt;/ng-template&gt; &lt;ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;shared-block&quot;&gt;"/>blokova<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">10,11</context>
@@ -4671,7 +4671,7 @@
</trans-unit>
<trans-unit id="df71fa93f0503396ea2bb3ba5161323330314d6c" datatype="html">
<source>Next Halving</source>
<target>Sljedeće prepolovljenje</target>
<target>Slj prepolovljenje</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">47</context>
@@ -5465,7 +5465,7 @@
</trans-unit>
<trans-unit id="1a8246eba9a999ee881248c4767d63b875ef07fe" datatype="html">
<source>blocks</source>
<target>blokovi</target>
<target>blokova</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html</context>
<context context-type="linenumber">63</context>
@@ -5885,7 +5885,7 @@
</trans-unit>
<trans-unit id="9ef8b357c32266f8423e24bf654006d3aa8fcd0b" datatype="html">
<source>Blocks (1w)</source>
<target>Blokova (1w)</target>
<target>Blokova (1 tj)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context>
<context context-type="linenumber">25</context>
@@ -6165,7 +6165,7 @@
</trans-unit>
<trans-unit id="3dc78651b2810cbb6e830fe7e57499d8cf6a8e4d" datatype="html">
<source>Blocks (24h)</source>
<target>Blokovi (24h)</target>
<target>Blokova (24h)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">120</context>
@@ -6815,7 +6815,7 @@
</trans-unit>
<trans-unit id="time-until" datatype="html">
<source>In ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></source>
<target>U ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></target>
<target>Za ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">188</context>

View File

@@ -510,7 +510,7 @@
</trans-unit>
<trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html">
<source>sats</source>
<target>sats</target>
<target>sat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">57</context>
@@ -881,7 +881,7 @@
</trans-unit>
<trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html">
<source>Pay</source>
<target>Betale</target>
<target>Betal</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">378</context>
@@ -4846,7 +4846,7 @@
</trans-unit>
<trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html">
<source>Amount (sats)</source>
<target>Beløp (sats)</target>
<target>Beløp (sat)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context>
<context context-type="linenumber">51</context>
@@ -6442,7 +6442,7 @@
</trans-unit>
<trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html">
<source>sats/tx</source>
<target>sats/tx</target>
<target>sat/tx</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context>
<context context-type="linenumber">33</context>
@@ -8145,7 +8145,7 @@
</trans-unit>
<trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html">
<source>mSats</source>
<target>mSats</target>
<target>mSat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context>
<context context-type="linenumber">35</context>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -84,11 +84,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
### Node.js + npm
Build Node.js v16.16.0 and npm v8 from source using `nvm`:
Build Node.js v20.17.0 and npm v9 from source using `nvm`:
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | zsh
source $HOME/.zshrc
nvm install v16.16.0 --shared-zlib
nvm install v20.17.0 --shared-zlib
nvm alias default node
```

View File

@@ -40,7 +40,7 @@ update_repo()
git fetch origin || exit 1
for remote in origin;do
git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1
git fetch "${remote}" || exit 1
git fetch "${remote}" --tags || exit 1
done
if [ $(git tag -l "${REF}") ];then

View File

@@ -1,12 +1,12 @@
{
"name": "mempool-unfurl",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mempool-unfurl",
"version": "3.0.0-beta",
"version": "3.0.0",
"dependencies": {
"@types/node": "^16.11.41",
"ejs": "^3.1.10",

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-unfurl",
"version": "3.0.0-beta",
"version": "3.1.0-dev",
"description": "Renderer for mempool open graph link preview images",
"repository": {
"type": "git",