Compare commits

...

39 Commits

Author SHA1 Message Date
Mononaut
b8d24e8cba Activation height logic for standardness rule changes 2024-08-30 23:12:24 +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
51 changed files with 582 additions and 176 deletions

12
LICENSE
View File

@@ -1,5 +1,5 @@
The Mempool Open Source Project® 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 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 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®, The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo, 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 the mempool block visualization Logo, the mempool Blocks Logo, the mempool
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
of Mempool Space K.K in Japan, the United States, and/or other countries. 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 See our full Trademark Policy and Guidelines for more details, published on
<https://mempool.space/trademark-policy>. <https://mempool.space/trademark-policy>.

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build #### 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._ _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import rbfCache from './rbf-cache'; 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 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 matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const unseen: string[] = []; // present in the mined block, not in our mempool 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 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 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 const accelerated: string[] = []; // prioritized by the mempool accelerator
@@ -133,23 +135,7 @@ class Audit {
totalWeight += tx.weight; totalWeight += tx.weight;
} }
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
// 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;
}
}
// transactions missing from near the end of our template are probably not being censored // transactions missing from near the end of our template are probably not being censored
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);

View File

@@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v1_taproot': 'v1_p2tr', 'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard', 'nonstandard': 'nonstandard',
'multisig': 'multisig', 'multisig': 'multisig',
'anchor': 'anchor',
'nulldata': 'op_return' '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 { return {
id: hash, id: hash,
transactions: Common.classifyTransactions(transactions), transactions: Common.classifyTransactions(transactions, height),
}; };
} }
@@ -616,7 +616,7 @@ class Blocks {
// add CPFP // add CPFP
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
// classify // 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); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
const cpfpClusters = await CpfpRepository.$getClustersAt(height); const cpfpClusters = await CpfpRepository.$getClustersAt(height);
@@ -653,7 +653,7 @@ class Blocks {
} }
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
// classify // classify
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
for (const tx of classifiedTxs) { for (const tx of classifiedTxs) {
classifiedTxMap[tx.txid] = tx; 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 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 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}`); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
@@ -1169,7 +1169,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => { transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0; let flags: number = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
} }
@@ -1188,7 +1188,7 @@ class Blocks {
} else { } else {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); 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; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC
@@ -1324,7 +1324,7 @@ class Blocks {
let summaryVersion = 0; let summaryVersion = 0;
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); 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; summaryVersion = 1;
} else { } else {
// Call Core RPC // Call Core RPC

View File

@@ -200,10 +200,13 @@ export class Common {
* *
* returns true early if any standardness rule is violated, otherwise false * 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) * (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 // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (this.isNonStandardVersion(tx, height)) {
return true; return true;
} }
@@ -250,6 +253,8 @@ export class Common {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (this.isNonStandardAnchor(tx, height)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@@ -335,6 +340,49 @@ export class Common {
return false; 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 { static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight; let weight = tx.weight;
let hasWitness = false; let hasWitness = false;
@@ -415,7 +463,7 @@ export class Common {
return flags; return flags;
} }
static getTransactionFlags(tx: TransactionExtended): number { static getTransactionFlags(tx: TransactionExtended, height?: number): number {
let flags = tx.flags ? BigInt(tx.flags) : 0n; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@@ -548,7 +596,7 @@ export class Common {
if (hasFakePubkey) { if (hasFakePubkey) {
flags |= TransactionFlags.fake_pubkey; flags |= TransactionFlags.fake_pubkey;
} }
// fast but bad heuristic to detect possible coinjoins // 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) // (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; const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
@@ -564,17 +612,17 @@ export class Common {
flags |= TransactionFlags.batch_payout; flags |= TransactionFlags.batch_payout;
} }
if (this.isNonStandard(tx)) { if (this.isNonStandard(tx, height)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
return Number(flags); return Number(flags);
} }
static classifyTransaction(tx: TransactionExtended): TransactionClassified { static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
let flags = 0; let flags = 0;
try { try {
flags = Common.getTransactionFlags(tx); flags = Common.getTransactionFlags(tx, height);
} catch (e) { } catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
} }
@@ -585,8 +633,8 @@ export class Common {
}; };
} }
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
return txs.map(Common.classifyTransaction); return txs.map(tx => Common.classifyTransaction(tx, height));
} }
static stripTransaction(tx: TransactionExtended): TransactionStripped { static stripTransaction(tx: TransactionExtended): TransactionStripped {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 81; private static currentVersion = 82;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -700,6 +700,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(81); 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`); 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(); export default new DatabaseMigration();

View File

@@ -338,6 +338,87 @@ class TransactionUtils {
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
return witness[positionOfScript]; 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(); export default new TransactionUtils();

View File

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

View File

@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
### 3. Run the Frontend ### 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: 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 ### 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: Build the frontend:

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,7 @@
<span>Spiral</span> <span>Spiral</span>
</a> </a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry"> <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> <defs>
<style> <style>
.d { .d {
@@ -125,7 +125,9 @@
<span>Blockstream</span> <span>Blockstream</span>
</a> </a>
<a href="https://unchained.com/" target="_blank" title="Unchained"> <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> <span>Unchained</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -150,7 +152,7 @@
<span>Bull Bitcoin</span> <span>Bull Bitcoin</span>
</a> </a>
<a href="https://exodus.com/" target="_blank" title="Exodus"> <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"/> <circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)"> <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)"/> <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> Trademark Notice<br>
</div> </div>
<p> <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>
<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;. 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 { .image.not-rounded {
border-radius: 0; border-radius: 0;
width: 60px;
height: 60px;
} }
.intro { .intro {
@@ -158,9 +156,8 @@
margin: 40px 29px 10px; margin: 40px 29px 10px;
&.image.coldcard { &.image.coldcard {
border-radius: 0; border-radius: 0;
width: auto; height: auto;
max-height: 50px; margin: 20px 29px 20px;
margin: 40px 29px 14px 29px;
} }
} }
} }

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 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 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) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, 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>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) as PieSeriesOption);

View File

@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
} }
handleVin() { 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) { if (address?.scripts.size) {
const script = address?.scripts.values().next().value; const script = address?.scripts.values().next().value;
if (script.template?.label) { if (script.template?.label) {

View File

@@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
// initialize the scene without any entry transition // 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); const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) { if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags(); this.setFilterFlags();
@@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.filtersAvailable = filtersAvailable; this.filtersAvailable = filtersAvailable;
if (this.scene) { if (this.scene) {
this.clearUpdateQueue(); this.clearUpdateQueue();
this.scene.setup(transactions); this.scene.setup(transactions, sort);
this.readyNextFrame = true; this.readyNextFrame = true;
this.start(); this.start();
this.updateSearchHighlight(); 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 // 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 // clean up any old transactions
Object.values(this.txs).forEach(tx => { Object.values(this.txs).forEach(tx => {
tx.destroy(); tx.destroy();
delete this.txs[tx.txid]; delete this.txs[tx.txid];
}); });
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
txs.forEach(tx => { let txViews = txs.map(tx => new TxView(tx, this));
const txView = new TxView(tx, this); if (sort) {
this.txs[tx.txid] = txView; txViews = txViews.sort(feeRateDescending);
}
txViews.forEach(txView => {
this.txs[txView.txid] = txView;
this.place(txView); this.place(txView);
this.saveGridToScreenPosition(txView); this.saveGridToScreenPosition(txView);
this.applyTxUpdate(txView, { this.applyTxUpdate(txView, {

View File

@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
flags: number; flags: number;
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
time?: number; 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'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;

View File

@@ -142,6 +142,10 @@ export function defaultColorFunction(
return auditColors.added_prioritized; return auditColors.added_prioritized;
case 'prioritized': case 'prioritized':
return auditColors.prioritized; return auditColors.prioritized;
case 'added_deprioritized':
return auditColors.added_prioritized;
case 'deprioritized':
return auditColors.prioritized;
case 'selected': case 'selected':
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1]; return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
case 'accelerated': 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" i18n="tx-features.tag.added|Added">Added</span>
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span> <span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
</ng-container> </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="'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="'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> <span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>

View File

@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service'; import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service'; import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
@Component({ @Component({
selector: 'app-block', selector: 'app-block',
@@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isUnseen = {}; const isUnseen = {};
const isAdded = {}; const isAdded = {};
const isPrioritized = {}; const isPrioritized = {};
const isDeprioritized = {};
const isCensored = {}; const isCensored = {};
const isMissing = {}; const isMissing = {};
const isSelected = {}; const isSelected = {};
@@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
this.numUnexpected = 0; this.numUnexpected = 0;
if (blockAudit?.template) { 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) { for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true; inTemplate[tx.txid] = true;
if (tx.acc) { if (tx.acc) {
@@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.addedTxs) { for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true; isAdded[txid] = true;
} }
for (const txid of blockAudit.prioritizedTxs || []) { for (const txid of blockAudit.prioritizedTxs) {
isPrioritized[txid] = true; isPrioritized[txid] = true;
} }
if (useLocalDeprioritized) {
for (const txid of deprioritized || []) {
isDeprioritized[txid] = true;
}
}
for (const txid of blockAudit.missingTxs) { for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true; isCensored[txid] = true;
} }
@@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
} else { } else {
tx.status = 'prioritized'; 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])) { } else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
tx.status = 'added'; tx.status = 'added';
} else if (inTemplate[tx.txid]) { } else if (inTemplate[tx.txid]) {

View File

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

View File

@@ -71,7 +71,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$; this.network$ = this.stateService.networkChanged$;
} }

View File

@@ -213,7 +213,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
if (state.mempoolPosition) { if (state.mempoolPosition) {
this.txPosition = state.mempoolPosition; this.txPosition = state.mempoolPosition;
if (this.txPosition.accelerated && !oldTxPosition.accelerated) { if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
this.acceleratingArrow = true; this.acceleratingArrow = true;
setTimeout(() => { setTimeout(() => {
this.acceleratingArrow = false; this.acceleratingArrow = false;

View File

@@ -293,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
}) })
).subscribe((accelerationHistory) => { ).subscribe((accelerationHistory) => {
for (const acceleration of 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; const boostCost = acceleration.boostCost || acceleration.bidBoost;
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
acceleration.boost = boostCost; acceleration.boost = boostCost;
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
checkAccelerationEligibility() { checkAccelerationEligibility() {
if (this.tx) { 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 replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
const highSigop = (this.tx.sigops * 20) > this.tx.weight; const highSigop = (this.tx.sigops * 20) > this.tx.weight;
this.eligibleForAcceleration = !replaceableInputs && !highSigop; this.eligibleForAcceleration = !replaceableInputs && !highSigop;

View File

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

View File

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

View File

@@ -358,12 +358,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}), }),
).subscribe((accelerationHistory) => { ).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) { for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) { if (acceleration.txid === this.txId) {
const boostCost = acceleration.boostCost || acceleration.bidBoost; if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
acceleration.boost = boostCost; const boostCost = acceleration.boostCost || acceleration.bidBoost;
this.tx.acceleratedAt = acceleration.added; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
this.accelerationInfo = acceleration; acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
} else {
this.tx.feeDelta = undefined;
}
}
this.waitingForAccelerationInfo = false; this.waitingForAccelerationInfo = false;
this.setIsAccelerated(); 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.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.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.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.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
this.checkAccelerationEligibility(); this.checkAccelerationEligibility();
} else { } else {

View File

@@ -239,7 +239,7 @@ export interface TransactionStripped {
acc?: boolean; acc?: boolean;
flags?: number | null; flags?: number | null;
time?: number; 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'; context?: 'projected' | 'actual';
} }

View File

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

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

View File

@@ -35,6 +35,7 @@ export class WebsocketService {
private isTrackingAddresses: string[] | false = false; private isTrackingAddresses: string[] | false = false;
private isTrackingAccelerations: boolean = false; private isTrackingAccelerations: boolean = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private stoppingTrackMempoolBlock: any | null = null;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number; private onlineCheckTimeoutTwo: number;
@@ -203,19 +204,31 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' }); 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 // skip duplicate tracking requests
if (force || this.trackingMempoolBlock !== block) { if (force || this.trackingMempoolBlock !== block) {
this.websocketSubject.next({ 'track-mempool-block': block }); this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true; this.isTrackingMempoolBlock = true;
this.trackingMempoolBlock = block; this.trackingMempoolBlock = block;
return true;
} }
return false;
} }
stopTrackMempoolBlock() { stopTrackMempoolBlock(): void {
this.websocketSubject.next({ 'track-mempool-block': -1 }); if (this.stoppingTrackMempoolBlock) {
clearTimeout(this.stoppingTrackMempoolBlock);
}
this.isTrackingMempoolBlock = false; 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') { startTrackRbf(mode: 'all' | 'fullRbf') {
@@ -424,6 +437,7 @@ export class WebsocketService {
if (response['projected-block-transactions'].blockTransactions) { if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
this.stateService.mempoolBlockUpdate$.next({ this.stateService.mempoolBlockUpdate$.next({
block: this.trackingMempoolBlock,
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
}); });
} else if (response['projected-block-transactions'].delta) { } else if (response['projected-block-transactions'].delta) {
@@ -432,7 +446,7 @@ export class WebsocketService {
this.startTrackMempoolBlock(this.trackingMempoolBlock, true); this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
} else { } else {
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; 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' | 'v0_p2wsh'
| 'v1_p2tr' | 'v1_p2tr'
| 'confidential' | 'confidential'
| 'anchor'
| 'unknown' | 'unknown'
const ADDRESS_PREFIXES = { const ADDRESS_PREFIXES = {
@@ -188,6 +189,12 @@ export class AddressTypeInfo {
const v = vin[0]; const v = vin[0];
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm)); 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 // and there's nothing more to learn from processing inputs for other types
} }
@@ -197,6 +204,10 @@ export class AddressTypeInfo {
if (!this.scripts.size) { if (!this.scripts.size) {
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm)); 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 { return {
block,
added: delta.added.map(uncompressTx), added: delta.added.map(uncompressTx),
removed: delta.removed, removed: delta.removed,
changed: delta.changed.map(tx => ({ changed: delta.changed.map(tx => ({

View File

@@ -20,6 +20,9 @@
@case ('multisig') { @case ('multisig') {
<span i18n="address.bare-multisig">bare multisig</span> <span i18n="address.bare-multisig">bare multisig</span>
} }
@case ('anchor') {
<span>anchor</span>
}
@case (null) { @case (null) {
<span>unknown</span> <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: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept 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:` }), 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 { export class ScriptInfo {

View File

@@ -1,10 +1,10 @@
import { TransactionFlags } from './filters.utils'; import { TransactionFlags } from './filters.utils';
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
import { Transaction } from '../interfaces/electrs.interface'; 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 // Bitcoin Core default policy settings
const TX_MAX_STANDARD_VERSION = 2;
const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_STANDARD_TX_WEIGHT = 400_000;
const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_BLOCK_SIGOPS_COST = 80_000;
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); 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 * 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) * (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 // version
if (tx.version > TX_MAX_STANDARD_VERSION) { if (isNonStandardVersion(tx, height, network)) {
return true; return true;
} }
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
} }
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
return true; return true;
} else if (isNonStandardAnchor(tx, height, network)) {
return true;
} }
// TODO: bad-witness-nonstandard // TODO: bad-witness-nonstandard
} }
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
return false; 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 // 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. // followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240 // 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); ].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; let flags = tx.flags ? BigInt(tx.flags) : 0n;
// Update variable flags (CPFP, RBF) // Update variable flags (CPFP, RBF)
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.batch_payout; flags |= TransactionFlags.batch_payout;
} }
if (isNonStandard(tx)) { if (isNonStandard(tx, height, network)) {
flags |= TransactionFlags.nonstandard; flags |= TransactionFlags.nonstandard;
} }
@@ -458,4 +508,83 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean):
} else { } else {
return tx.effectiveFeePerVsize; 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>
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html"> <trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
<source>Total Bid Boost</source> <source>Total Bid Boost</source>
<target>Ukupno povećanje ponude</target> <target>Total Bid Boost</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context> <context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
@@ -1969,7 +1969,7 @@
</trans-unit> </trans-unit>
<trans-unit id="841f2a74ae5095e6e37f5749f3cc1851cf36a420" datatype="html"> <trans-unit id="841f2a74ae5095e6e37f5749f3cc1851cf36a420" datatype="html">
<source>Avg Max Bid</source> <source>Avg Max Bid</source>
<target>Prosječna maks. ponuda</target> <target>Prosj. max ponuda</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/pending-stats/pending-stats.component.html</context> <context context-type="sourcefile">src/app/components/acceleration/pending-stats/pending-stats.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
@@ -3622,7 +3622,7 @@
</trans-unit> </trans-unit>
<trans-unit id="14779b0ce4cbc4d975a35a8fe074426228a324f3" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/components/block/block-transactions.component.html</context> <context context-type="sourcefile">src/app/components/block/block-transactions.component.html</context>
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
@@ -4046,7 +4046,7 @@
</trans-unit> </trans-unit>
<trans-unit id="8a7b4bd44c0ac71b2e72de0398b303257f7d2f54" datatype="html"> <trans-unit id="8a7b4bd44c0ac71b2e72de0398b303257f7d2f54" datatype="html">
<source>Blocks</source> <source>Blocks</source>
<target>Blokovi</target> <target>Blokova</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context> <context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
@@ -4360,7 +4360,7 @@
</trans-unit> </trans-unit>
<trans-unit id="23c872b0336e20284724607f2887da39bd8142c3" datatype="html"> <trans-unit id="23c872b0336e20284724607f2887da39bd8142c3" datatype="html">
<source>Previous fee</source> <source>Previous fee</source>
<target>Prethodna naknada</target> <target>Preth. naknada</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context> <context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
<context context-type="linenumber">107</context> <context context-type="linenumber">107</context>
@@ -4594,7 +4594,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1bb6965f8e1bbe40c076528ffd841da86f57f119" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context> <context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">10,11</context> <context context-type="linenumber">10,11</context>
@@ -4671,7 +4671,7 @@
</trans-unit> </trans-unit>
<trans-unit id="df71fa93f0503396ea2bb3ba5161323330314d6c" datatype="html"> <trans-unit id="df71fa93f0503396ea2bb3ba5161323330314d6c" datatype="html">
<source>Next Halving</source> <source>Next Halving</source>
<target>Sljedeće prepolovljenje</target> <target>Slj prepolovljenje</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context> <context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">47</context>
@@ -5465,7 +5465,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1a8246eba9a999ee881248c4767d63b875ef07fe" datatype="html"> <trans-unit id="1a8246eba9a999ee881248c4767d63b875ef07fe" datatype="html">
<source>blocks</source> <source>blocks</source>
<target>blokovi</target> <target>blokova</target>
<context-group purpose="location"> <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="sourcefile">src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">63</context>
@@ -5885,7 +5885,7 @@
</trans-unit> </trans-unit>
<trans-unit id="9ef8b357c32266f8423e24bf654006d3aa8fcd0b" datatype="html"> <trans-unit id="9ef8b357c32266f8423e24bf654006d3aa8fcd0b" datatype="html">
<source>Blocks (1w)</source> <source>Blocks (1w)</source>
<target>Blokova (1w)</target> <target>Blokova (1 tj)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context> <context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">25</context>
@@ -6165,7 +6165,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3dc78651b2810cbb6e830fe7e57499d8cf6a8e4d" datatype="html"> <trans-unit id="3dc78651b2810cbb6e830fe7e57499d8cf6a8e4d" datatype="html">
<source>Blocks (24h)</source> <source>Blocks (24h)</source>
<target>Blokovi (24h)</target> <target>Blokova (24h)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context> <context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">120</context>
@@ -6815,7 +6815,7 @@
</trans-unit> </trans-unit>
<trans-unit id="time-until" datatype="html"> <trans-unit id="time-until" datatype="html">
<source>In ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></source> <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-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context> <context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">188</context> <context context-type="linenumber">188</context>

View File

@@ -510,7 +510,7 @@
</trans-unit> </trans-unit>
<trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html"> <trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html">
<source>sats</source> <source>sats</source>
<target>sats</target> <target>sat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">57</context>
@@ -881,7 +881,7 @@
</trans-unit> </trans-unit>
<trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html"> <trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html">
<source>Pay</source> <source>Pay</source>
<target>Betale</target> <target>Betal</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context> <context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
<context context-type="linenumber">378</context> <context context-type="linenumber">378</context>
@@ -4846,7 +4846,7 @@
</trans-unit> </trans-unit>
<trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html"> <trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html">
<source>Amount (sats)</source> <source>Amount (sats)</source>
<target>Beløp (sats)</target> <target>Beløp (sat)</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context> <context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">51</context>
@@ -6442,7 +6442,7 @@
</trans-unit> </trans-unit>
<trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html"> <trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html">
<source>sats/tx</source> <source>sats/tx</source>
<target>sats/tx</target> <target>sat/tx</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context> <context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">33</context>
@@ -8145,7 +8145,7 @@
</trans-unit> </trans-unit>
<trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html"> <trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html">
<source>mSats</source> <source>mSats</source>
<target>mSats</target> <target>mSat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context> <context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context>
<context context-type="linenumber">35</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 ### 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 source $HOME/.zshrc
nvm install v16.16.0 --shared-zlib nvm install v20.17.0 --shared-zlib
nvm alias default node nvm alias default node
``` ```

View File

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

View File

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

View File

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