Compare commits
52 Commits
ops/cron-s
...
natsoni/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
428cc9eacd | ||
|
|
638acffbad | ||
|
|
ae9125d316 | ||
|
|
07fd3d3409 | ||
|
|
f7360433a1 | ||
|
|
f6fac92180 | ||
|
|
82d1502bfa | ||
|
|
8ab104d191 | ||
|
|
263742132c | ||
|
|
e3c3f31ddb | ||
|
|
70d1f52268 | ||
|
|
e44f30d7a7 | ||
|
|
099d84a395 | ||
|
|
12285465d9 | ||
|
|
fad39e0bea | ||
|
|
3bea10ea35 | ||
|
|
1ea45e9e96 | ||
|
|
8c2d0e1d6c | ||
|
|
009fba3dd5 | ||
|
|
a0fc4861d4 | ||
|
|
62085581dd | ||
|
|
05efa8c300 | ||
|
|
eee99a6407 | ||
|
|
98cee4a6cd | ||
|
|
0302999806 | ||
|
|
1876d67e74 | ||
|
|
c0bb75e5b1 | ||
|
|
4059a902a1 | ||
|
|
4cc19a7235 | ||
|
|
c874d642c5 | ||
|
|
f0af1703da | ||
|
|
5452d7f524 | ||
|
|
ff9e2456b9 | ||
|
|
4e581347c8 | ||
|
|
820777236e | ||
|
|
beeb5eb08c | ||
|
|
b78aca0282 | ||
|
|
9572f2d554 | ||
|
|
ef13596b59 | ||
|
|
80da024bbb | ||
|
|
f75f85f914 | ||
|
|
b3ac107b0b | ||
|
|
f8cedaa7a3 | ||
|
|
72bb92dd8b | ||
|
|
e3c4e219f3 | ||
|
|
aa3fa4478a | ||
|
|
c9171224e1 | ||
|
|
248cef7718 | ||
|
|
26c03eee88 | ||
|
|
a31729b8b8 | ||
|
|
79e494150c | ||
|
|
104c7f4285 |
12
LICENSE
12
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Mempool Open Source Project®
|
||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
||||
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
@@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
|
||||
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||
the United States, and/or other countries.
|
||||
|
||||
See our full Trademark Policy and Guidelines for more details, published on
|
||||
<https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
#### Build
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer_
|
||||
|
||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
||||
|
||||
|
||||
19
backend/package-lock.json
generated
19
backend/package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"hasInstallScript": true,
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "~1.7.4",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
@@ -2277,9 +2278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -9438,9 +9439,9 @@
|
||||
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -42,7 +42,7 @@
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "~1.7.4",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
|
||||
@@ -2,6 +2,7 @@ import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import rbfCache from './rbf-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||
|
||||
@@ -15,7 +16,8 @@ class Audit {
|
||||
const matches: string[] = []; // present in both mined block and template
|
||||
const added: string[] = []; // present in mined block, not in template
|
||||
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||
const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
|
||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||
@@ -133,23 +135,7 @@ class Audit {
|
||||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
|
||||
// identify "prioritized" transactions
|
||||
let lastEffectiveRate = 0;
|
||||
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||
// Transactions should appear in ascending order of mining priority.
|
||||
for (let i = transactions.length - 1; i > 0; i--) {
|
||||
const blockTx = transactions[i];
|
||||
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||
// so exclude from the analysis.
|
||||
if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) {
|
||||
prioritized.push(blockTx.txid);
|
||||
// accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference
|
||||
} else if (!isAccelerated[blockTx.txid]) {
|
||||
lastEffectiveRate = blockTx.effectiveFeePerVsize || 0;
|
||||
}
|
||||
}
|
||||
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||
|
||||
@@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
'witness_v1_taproot': 'v1_p2tr',
|
||||
'nonstandard': 'nonstandard',
|
||||
'multisig': 'multisig',
|
||||
'anchor': 'anchor',
|
||||
'nulldata': 'op_return'
|
||||
};
|
||||
|
||||
|
||||
@@ -219,10 +219,10 @@ class Blocks {
|
||||
};
|
||||
}
|
||||
|
||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
||||
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||
return {
|
||||
id: hash,
|
||||
transactions: Common.classifyTransactions(transactions),
|
||||
transactions: Common.classifyTransactions(transactions, height),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ class Blocks {
|
||||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
@@ -653,7 +653,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
for (const tx of classifiedTxs) {
|
||||
classifiedTxMap[tx.txid] = tx;
|
||||
@@ -912,7 +912,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@@ -1169,7 +1169,7 @@ class Blocks {
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -1188,7 +1188,7 @@ class Blocks {
|
||||
} else {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(hash, txs);
|
||||
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@@ -1324,7 +1324,7 @@ class Blocks {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
||||
@@ -10,7 +10,6 @@ import logger from '../logger';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@@ -200,10 +199,13 @@ export class Common {
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
static isNonStandard(tx: TransactionExtended): boolean {
|
||||
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (this.isNonStandardVersion(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -250,6 +252,8 @@ export class Common {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@@ -335,6 +339,49 @@ export class Common {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
@@ -415,7 +462,7 @@ export class Common {
|
||||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@@ -548,7 +595,7 @@ export class Common {
|
||||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
|
||||
// fast but bad heuristic to detect possible coinjoins
|
||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||
@@ -564,17 +611,17 @@ export class Common {
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
if (this.isNonStandard(tx, height)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
||||
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||
let flags = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@@ -585,8 +632,8 @@ export class Common {
|
||||
};
|
||||
}
|
||||
|
||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
||||
return txs.map(Common.classifyTransaction);
|
||||
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 81;
|
||||
private static currentVersion = 83;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -700,6 +700,16 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(81);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$fixBadV1AuditBlocks();
|
||||
await this.updateToSchemaVersion(82);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 83) {
|
||||
await this.$addPerSecondVsizeToStatistics();
|
||||
await this.updateToSchemaVersion(83);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1314,6 +1324,69 @@ class DatabaseMigration {
|
||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||
const badBlocks = [
|
||||
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||
];
|
||||
|
||||
for (const hash of badBlocks) {
|
||||
try {
|
||||
await this.$executeQuery(`
|
||||
UPDATE blocks_audits
|
||||
SET prioritized_txs = '[]'
|
||||
WHERE hash = '${hash}'
|
||||
`, true);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async $addPerSecondVsizeToStatistics(): Promise<void> {
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_2` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_3` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_4` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_5` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_6` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_8` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_10` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_12` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_15` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_20` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_30` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_40` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_50` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_60` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_70` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_80` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_90` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_100` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_125` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_150` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_175` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_200` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_250` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_300` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_350` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_400` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_500` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_600` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_700` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_800` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_900` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1000` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1200` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1400` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1600` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_1800` int(11) NOT NULL DEFAULT 0');
|
||||
await this.$executeQuery('ALTER TABLE `statistics` ADD `vsize_ps_2000` int(11) NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseMigration();
|
||||
|
||||
@@ -12,6 +12,7 @@ import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
import { logFees } from './statistics/statistics';
|
||||
|
||||
class Mempool {
|
||||
private inSync: boolean = false;
|
||||
@@ -34,6 +35,7 @@ class Mempool {
|
||||
|
||||
private vBytesPerSecondArray: VbytesPerSecond[] = [];
|
||||
private vBytesPerSecond: number = 0;
|
||||
private vBytesPerSecondByFeeRate: { [feePerWU: number]: number } = {};
|
||||
private mempoolProtection = 0;
|
||||
private latestTransactions: any[] = [];
|
||||
|
||||
@@ -193,6 +195,10 @@ class Mempool {
|
||||
return this.vBytesPerSecond;
|
||||
}
|
||||
|
||||
public getVBytesPerSecondByFeeRate(): { [feePerWU: number]: number } {
|
||||
return this.vBytesPerSecondByFeeRate;
|
||||
}
|
||||
|
||||
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
||||
const txTimes: number[] = [];
|
||||
txIds.forEach((txId: string) => {
|
||||
@@ -270,6 +276,7 @@ class Mempool {
|
||||
this.vBytesPerSecondArray.push({
|
||||
unixTime: new Date().getTime(),
|
||||
vSize: transaction.vsize,
|
||||
effectiveFeePerVsize: transaction.effectiveFeePerVsize
|
||||
});
|
||||
}
|
||||
hasChange = true;
|
||||
@@ -588,6 +595,21 @@ class Mempool {
|
||||
this.vBytesPerSecond = Math.round(
|
||||
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD
|
||||
);
|
||||
|
||||
if (!Common.isLiquid()) {
|
||||
this.vBytesPerSecondByFeeRate = {};
|
||||
for (const tx of this.vBytesPerSecondArray) {
|
||||
for (let i = 0; i < logFees.length; i++) {
|
||||
if (tx.effectiveFeePerVsize < logFees[i + 1] || i === logFees.length - 1) {
|
||||
this.vBytesPerSecondByFeeRate[logFees[i]] = (this.vBytesPerSecondByFeeRate[logFees[i]] || 0) + tx.vSize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const feeRate of Object.keys(this.vBytesPerSecondByFeeRate)) {
|
||||
this.vBytesPerSecondByFeeRate[feeRate] = Math.round(this.vBytesPerSecondByFeeRate[feeRate] / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,45 @@ class StatisticsApi {
|
||||
vsize_1400,
|
||||
vsize_1600,
|
||||
vsize_1800,
|
||||
vsize_2000
|
||||
vsize_2000,
|
||||
vsize_ps_1,
|
||||
vsize_ps_2,
|
||||
vsize_ps_3,
|
||||
vsize_ps_4,
|
||||
vsize_ps_5,
|
||||
vsize_ps_6,
|
||||
vsize_ps_8,
|
||||
vsize_ps_10,
|
||||
vsize_ps_12,
|
||||
vsize_ps_15,
|
||||
vsize_ps_20,
|
||||
vsize_ps_30,
|
||||
vsize_ps_40,
|
||||
vsize_ps_50,
|
||||
vsize_ps_60,
|
||||
vsize_ps_70,
|
||||
vsize_ps_80,
|
||||
vsize_ps_90,
|
||||
vsize_ps_100,
|
||||
vsize_ps_125,
|
||||
vsize_ps_150,
|
||||
vsize_ps_175,
|
||||
vsize_ps_200,
|
||||
vsize_ps_250,
|
||||
vsize_ps_300,
|
||||
vsize_ps_350,
|
||||
vsize_ps_400,
|
||||
vsize_ps_500,
|
||||
vsize_ps_600,
|
||||
vsize_ps_700,
|
||||
vsize_ps_800,
|
||||
vsize_ps_900,
|
||||
vsize_ps_1000,
|
||||
vsize_ps_1200,
|
||||
vsize_ps_1400,
|
||||
vsize_ps_1600,
|
||||
vsize_ps_1800,
|
||||
vsize_ps_2000
|
||||
)
|
||||
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`;
|
||||
@@ -67,56 +105,94 @@ class StatisticsApi {
|
||||
public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> {
|
||||
try {
|
||||
const query = `INSERT INTO statistics(
|
||||
added,
|
||||
unconfirmed_transactions,
|
||||
tx_per_second,
|
||||
vbytes_per_second,
|
||||
mempool_byte_weight,
|
||||
fee_data,
|
||||
total_fee,
|
||||
min_fee,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
vsize_4,
|
||||
vsize_5,
|
||||
vsize_6,
|
||||
vsize_8,
|
||||
vsize_10,
|
||||
vsize_12,
|
||||
vsize_15,
|
||||
vsize_20,
|
||||
vsize_30,
|
||||
vsize_40,
|
||||
vsize_50,
|
||||
vsize_60,
|
||||
vsize_70,
|
||||
vsize_80,
|
||||
vsize_90,
|
||||
vsize_100,
|
||||
vsize_125,
|
||||
vsize_150,
|
||||
vsize_175,
|
||||
vsize_200,
|
||||
vsize_250,
|
||||
vsize_300,
|
||||
vsize_350,
|
||||
vsize_400,
|
||||
vsize_500,
|
||||
vsize_600,
|
||||
vsize_700,
|
||||
vsize_800,
|
||||
vsize_900,
|
||||
vsize_1000,
|
||||
vsize_1200,
|
||||
vsize_1400,
|
||||
vsize_1600,
|
||||
vsize_1800,
|
||||
vsize_2000
|
||||
)
|
||||
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
added,
|
||||
unconfirmed_transactions,
|
||||
tx_per_second,
|
||||
vbytes_per_second,
|
||||
mempool_byte_weight,
|
||||
fee_data,
|
||||
total_fee,
|
||||
min_fee,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
vsize_4,
|
||||
vsize_5,
|
||||
vsize_6,
|
||||
vsize_8,
|
||||
vsize_10,
|
||||
vsize_12,
|
||||
vsize_15,
|
||||
vsize_20,
|
||||
vsize_30,
|
||||
vsize_40,
|
||||
vsize_50,
|
||||
vsize_60,
|
||||
vsize_70,
|
||||
vsize_80,
|
||||
vsize_90,
|
||||
vsize_100,
|
||||
vsize_125,
|
||||
vsize_150,
|
||||
vsize_175,
|
||||
vsize_200,
|
||||
vsize_250,
|
||||
vsize_300,
|
||||
vsize_350,
|
||||
vsize_400,
|
||||
vsize_500,
|
||||
vsize_600,
|
||||
vsize_700,
|
||||
vsize_800,
|
||||
vsize_900,
|
||||
vsize_1000,
|
||||
vsize_1200,
|
||||
vsize_1400,
|
||||
vsize_1600,
|
||||
vsize_1800,
|
||||
vsize_2000,
|
||||
vsize_ps_1,
|
||||
vsize_ps_2,
|
||||
vsize_ps_3,
|
||||
vsize_ps_4,
|
||||
vsize_ps_5,
|
||||
vsize_ps_6,
|
||||
vsize_ps_8,
|
||||
vsize_ps_10,
|
||||
vsize_ps_12,
|
||||
vsize_ps_15,
|
||||
vsize_ps_20,
|
||||
vsize_ps_30,
|
||||
vsize_ps_40,
|
||||
vsize_ps_50,
|
||||
vsize_ps_60,
|
||||
vsize_ps_70,
|
||||
vsize_ps_80,
|
||||
vsize_ps_90,
|
||||
vsize_ps_100,
|
||||
vsize_ps_125,
|
||||
vsize_ps_150,
|
||||
vsize_ps_175,
|
||||
vsize_ps_200,
|
||||
vsize_ps_250,
|
||||
vsize_ps_300,
|
||||
vsize_ps_350,
|
||||
vsize_ps_400,
|
||||
vsize_ps_500,
|
||||
vsize_ps_600,
|
||||
vsize_ps_700,
|
||||
vsize_ps_800,
|
||||
vsize_ps_900,
|
||||
vsize_ps_1000,
|
||||
vsize_ps_1200,
|
||||
vsize_ps_1400,
|
||||
vsize_ps_1600,
|
||||
vsize_ps_1800,
|
||||
vsize_ps_2000
|
||||
)
|
||||
VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
const params: (string | number)[] = [
|
||||
statistics.unconfirmed_transactions,
|
||||
statistics.tx_per_second,
|
||||
@@ -163,6 +239,44 @@ class StatisticsApi {
|
||||
statistics.vsize_1600,
|
||||
statistics.vsize_1800,
|
||||
statistics.vsize_2000,
|
||||
statistics.vsize_ps_1,
|
||||
statistics.vsize_ps_2,
|
||||
statistics.vsize_ps_3,
|
||||
statistics.vsize_ps_4,
|
||||
statistics.vsize_ps_5,
|
||||
statistics.vsize_ps_6,
|
||||
statistics.vsize_ps_8,
|
||||
statistics.vsize_ps_10,
|
||||
statistics.vsize_ps_12,
|
||||
statistics.vsize_ps_15,
|
||||
statistics.vsize_ps_20,
|
||||
statistics.vsize_ps_30,
|
||||
statistics.vsize_ps_40,
|
||||
statistics.vsize_ps_50,
|
||||
statistics.vsize_ps_60,
|
||||
statistics.vsize_ps_70,
|
||||
statistics.vsize_ps_80,
|
||||
statistics.vsize_ps_90,
|
||||
statistics.vsize_ps_100,
|
||||
statistics.vsize_ps_125,
|
||||
statistics.vsize_ps_150,
|
||||
statistics.vsize_ps_175,
|
||||
statistics.vsize_ps_200,
|
||||
statistics.vsize_ps_250,
|
||||
statistics.vsize_ps_300,
|
||||
statistics.vsize_ps_350,
|
||||
statistics.vsize_ps_400,
|
||||
statistics.vsize_ps_500,
|
||||
statistics.vsize_ps_600,
|
||||
statistics.vsize_ps_700,
|
||||
statistics.vsize_ps_800,
|
||||
statistics.vsize_ps_900,
|
||||
statistics.vsize_ps_1000,
|
||||
statistics.vsize_ps_1200,
|
||||
statistics.vsize_ps_1400,
|
||||
statistics.vsize_ps_1600,
|
||||
statistics.vsize_ps_1800,
|
||||
statistics.vsize_ps_2000,
|
||||
];
|
||||
const [result]: any = await DB.query(query, params);
|
||||
return result.insertId;
|
||||
@@ -214,7 +328,45 @@ class StatisticsApi {
|
||||
CAST(avg(vsize_1400) as DOUBLE) as vsize_1400,
|
||||
CAST(avg(vsize_1600) as DOUBLE) as vsize_1600,
|
||||
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
|
||||
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
|
||||
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000,
|
||||
CAST(avg(vsize_ps_1) as DOUBLE) as vsize_ps_1,
|
||||
CAST(avg(vsize_ps_2) as DOUBLE) as vsize_ps_2,
|
||||
CAST(avg(vsize_ps_3) as DOUBLE) as vsize_ps_3,
|
||||
CAST(avg(vsize_ps_4) as DOUBLE) as vsize_ps_4,
|
||||
CAST(avg(vsize_ps_5) as DOUBLE) as vsize_ps_5,
|
||||
CAST(avg(vsize_ps_6) as DOUBLE) as vsize_ps_6,
|
||||
CAST(avg(vsize_ps_8) as DOUBLE) as vsize_ps_8,
|
||||
CAST(avg(vsize_ps_10) as DOUBLE) as vsize_ps_10,
|
||||
CAST(avg(vsize_ps_12) as DOUBLE) as vsize_ps_12,
|
||||
CAST(avg(vsize_ps_15) as DOUBLE) as vsize_ps_15,
|
||||
CAST(avg(vsize_ps_20) as DOUBLE) as vsize_ps_20,
|
||||
CAST(avg(vsize_ps_30) as DOUBLE) as vsize_ps_30,
|
||||
CAST(avg(vsize_ps_40) as DOUBLE) as vsize_ps_40,
|
||||
CAST(avg(vsize_ps_50) as DOUBLE) as vsize_ps_50,
|
||||
CAST(avg(vsize_ps_60) as DOUBLE) as vsize_ps_60,
|
||||
CAST(avg(vsize_ps_70) as DOUBLE) as vsize_ps_70,
|
||||
CAST(avg(vsize_ps_80) as DOUBLE) as vsize_ps_80,
|
||||
CAST(avg(vsize_ps_90) as DOUBLE) as vsize_ps_90,
|
||||
CAST(avg(vsize_ps_100) as DOUBLE) as vsize_ps_100,
|
||||
CAST(avg(vsize_ps_125) as DOUBLE) as vsize_ps_125,
|
||||
CAST(avg(vsize_ps_150) as DOUBLE) as vsize_ps_150,
|
||||
CAST(avg(vsize_ps_175) as DOUBLE) as vsize_ps_175,
|
||||
CAST(avg(vsize_ps_200) as DOUBLE) as vsize_ps_200,
|
||||
CAST(avg(vsize_ps_250) as DOUBLE) as vsize_ps_250,
|
||||
CAST(avg(vsize_ps_300) as DOUBLE) as vsize_ps_300,
|
||||
CAST(avg(vsize_ps_350) as DOUBLE) as vsize_ps_350,
|
||||
CAST(avg(vsize_ps_400) as DOUBLE) as vsize_ps_400,
|
||||
CAST(avg(vsize_ps_500) as DOUBLE) as vsize_ps_500,
|
||||
CAST(avg(vsize_ps_600) as DOUBLE) as vsize_ps_600,
|
||||
CAST(avg(vsize_ps_700) as DOUBLE) as vsize_ps_700,
|
||||
CAST(avg(vsize_ps_800) as DOUBLE) as vsize_ps_800,
|
||||
CAST(avg(vsize_ps_900) as DOUBLE) as vsize_ps_900,
|
||||
CAST(avg(vsize_ps_1000) as DOUBLE) as vsize_ps_1000,
|
||||
CAST(avg(vsize_ps_1200) as DOUBLE) as vsize_ps_1200,
|
||||
CAST(avg(vsize_ps_1400) as DOUBLE) as vsize_ps_1400,
|
||||
CAST(avg(vsize_ps_1600) as DOUBLE) as vsize_ps_1600,
|
||||
CAST(avg(vsize_ps_1800) as DOUBLE) as vsize_ps_1800,
|
||||
CAST(avg(vsize_ps_2000) as DOUBLE) as vsize_ps_2000 \
|
||||
FROM statistics \
|
||||
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
|
||||
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||
@@ -264,7 +416,45 @@ class StatisticsApi {
|
||||
vsize_1400,
|
||||
vsize_1600,
|
||||
vsize_1800,
|
||||
vsize_2000 \
|
||||
vsize_2000,
|
||||
vsize_ps_1,
|
||||
vsize_ps_2,
|
||||
vsize_ps_3,
|
||||
vsize_ps_4,
|
||||
vsize_ps_5,
|
||||
vsize_ps_6,
|
||||
vsize_ps_8,
|
||||
vsize_ps_10,
|
||||
vsize_ps_12,
|
||||
vsize_ps_15,
|
||||
vsize_ps_20,
|
||||
vsize_ps_30,
|
||||
vsize_ps_40,
|
||||
vsize_ps_50,
|
||||
vsize_ps_60,
|
||||
vsize_ps_70,
|
||||
vsize_ps_80,
|
||||
vsize_ps_90,
|
||||
vsize_ps_100,
|
||||
vsize_ps_125,
|
||||
vsize_ps_150,
|
||||
vsize_ps_175,
|
||||
vsize_ps_200,
|
||||
vsize_ps_250,
|
||||
vsize_ps_300,
|
||||
vsize_ps_350,
|
||||
vsize_ps_400,
|
||||
vsize_ps_500,
|
||||
vsize_ps_600,
|
||||
vsize_ps_700,
|
||||
vsize_ps_800,
|
||||
vsize_ps_900,
|
||||
vsize_ps_1000,
|
||||
vsize_ps_1200,
|
||||
vsize_ps_1400,
|
||||
vsize_ps_1600,
|
||||
vsize_ps_1800,
|
||||
vsize_ps_2000 \
|
||||
FROM statistics \
|
||||
${interval === 'all' ? '' : `WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`} \
|
||||
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
|
||||
@@ -452,7 +642,47 @@ class StatisticsApi {
|
||||
s.vsize_1600,
|
||||
s.vsize_1800,
|
||||
s.vsize_2000,
|
||||
]
|
||||
],
|
||||
vsizes_ps: [
|
||||
s.vsize_ps_1,
|
||||
s.vsize_ps_2,
|
||||
s.vsize_ps_3,
|
||||
s.vsize_ps_4,
|
||||
s.vsize_ps_5,
|
||||
s.vsize_ps_6,
|
||||
s.vsize_ps_8,
|
||||
s.vsize_ps_10,
|
||||
s.vsize_ps_12,
|
||||
s.vsize_ps_15,
|
||||
s.vsize_ps_20,
|
||||
s.vsize_ps_30,
|
||||
s.vsize_ps_40,
|
||||
s.vsize_ps_50,
|
||||
s.vsize_ps_60,
|
||||
s.vsize_ps_70,
|
||||
s.vsize_ps_80,
|
||||
s.vsize_ps_90,
|
||||
s.vsize_ps_100,
|
||||
s.vsize_ps_125,
|
||||
s.vsize_ps_150,
|
||||
s.vsize_ps_175,
|
||||
s.vsize_ps_200,
|
||||
s.vsize_ps_250,
|
||||
s.vsize_ps_300,
|
||||
s.vsize_ps_350,
|
||||
s.vsize_ps_400,
|
||||
s.vsize_ps_500,
|
||||
s.vsize_ps_600,
|
||||
s.vsize_ps_700,
|
||||
s.vsize_ps_800,
|
||||
s.vsize_ps_900,
|
||||
s.vsize_ps_1000,
|
||||
s.vsize_ps_1200,
|
||||
s.vsize_ps_1400,
|
||||
s.vsize_ps_1600,
|
||||
s.vsize_ps_1800,
|
||||
s.vsize_ps_2000,
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -506,6 +736,44 @@ class StatisticsApi {
|
||||
vsize_1600: s.vsizes[35],
|
||||
vsize_1800: s.vsizes[36],
|
||||
vsize_2000: s.vsizes[37],
|
||||
vsize_ps_1: s.vsizes_ps?.[0] || 0,
|
||||
vsize_ps_2: s.vsizes_ps?.[1] || 0,
|
||||
vsize_ps_3: s.vsizes_ps?.[2] || 0,
|
||||
vsize_ps_4: s.vsizes_ps?.[3] || 0,
|
||||
vsize_ps_5: s.vsizes_ps?.[4] || 0,
|
||||
vsize_ps_6: s.vsizes_ps?.[5] || 0,
|
||||
vsize_ps_8: s.vsizes_ps?.[6] || 0,
|
||||
vsize_ps_10: s.vsizes_ps?.[7] || 0,
|
||||
vsize_ps_12: s.vsizes_ps?.[8] || 0,
|
||||
vsize_ps_15: s.vsizes_ps?.[9] || 0,
|
||||
vsize_ps_20: s.vsizes_ps?.[10] || 0,
|
||||
vsize_ps_30: s.vsizes_ps?.[11] || 0,
|
||||
vsize_ps_40: s.vsizes_ps?.[12] || 0,
|
||||
vsize_ps_50: s.vsizes_ps?.[13] || 0,
|
||||
vsize_ps_60: s.vsizes_ps?.[14] || 0,
|
||||
vsize_ps_70: s.vsizes_ps?.[15] || 0,
|
||||
vsize_ps_80: s.vsizes_ps?.[16] || 0,
|
||||
vsize_ps_90: s.vsizes_ps?.[17] || 0,
|
||||
vsize_ps_100: s.vsizes_ps?.[18] || 0,
|
||||
vsize_ps_125: s.vsizes_ps?.[19] || 0,
|
||||
vsize_ps_150: s.vsizes_ps?.[20] || 0,
|
||||
vsize_ps_175: s.vsizes_ps?.[21] || 0,
|
||||
vsize_ps_200: s.vsizes_ps?.[22] || 0,
|
||||
vsize_ps_250: s.vsizes_ps?.[23] || 0,
|
||||
vsize_ps_300: s.vsizes_ps?.[24] || 0,
|
||||
vsize_ps_350: s.vsizes_ps?.[25] || 0,
|
||||
vsize_ps_400: s.vsizes_ps?.[26] || 0,
|
||||
vsize_ps_500: s.vsizes_ps?.[27] || 0,
|
||||
vsize_ps_600: s.vsizes_ps?.[28] || 0,
|
||||
vsize_ps_700: s.vsizes_ps?.[29] || 0,
|
||||
vsize_ps_800: s.vsizes_ps?.[30] || 0,
|
||||
vsize_ps_900: s.vsizes_ps?.[31] || 0,
|
||||
vsize_ps_1000: s.vsizes_ps?.[32] || 0,
|
||||
vsize_ps_1200: s.vsizes_ps?.[33] || 0,
|
||||
vsize_ps_1400: s.vsizes_ps?.[34] || 0,
|
||||
vsize_ps_1600: s.vsizes_ps?.[35] || 0,
|
||||
vsize_ps_1800: s.vsizes_ps?.[36] || 0,
|
||||
vsize_ps_2000: s.vsizes_ps?.[37] || 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import { TransactionExtended, OptimizedStatistic } from '../../mempool.interface
|
||||
import { Common } from '../common';
|
||||
import statisticsApi from './statistics-api';
|
||||
|
||||
export const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
class Statistics {
|
||||
protected intervalTimer: NodeJS.Timer | undefined;
|
||||
protected lastRun: number = 0;
|
||||
@@ -42,6 +45,7 @@ class Statistics {
|
||||
const currentMempool = memPool.getMempool();
|
||||
const txPerSecond = memPool.getTxPerSecond();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const vBytesPerSecondByFeeRate = memPool.getVBytesPerSecondByFeeRate();
|
||||
|
||||
logger.debug('Running statistics');
|
||||
|
||||
@@ -73,9 +77,6 @@ class Statistics {
|
||||
const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4;
|
||||
const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr);
|
||||
|
||||
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
const weightVsizeFees: { [feePerWU: number]: number } = {};
|
||||
const lastItem = logFees.length - 1;
|
||||
|
||||
@@ -147,6 +148,44 @@ class Statistics {
|
||||
vsize_1600: weightVsizeFees['1600'] || 0,
|
||||
vsize_1800: weightVsizeFees['1800'] || 0,
|
||||
vsize_2000: weightVsizeFees['2000'] || 0,
|
||||
vsize_ps_1: vBytesPerSecondByFeeRate['1'] || 0,
|
||||
vsize_ps_2: vBytesPerSecondByFeeRate['2'] || 0,
|
||||
vsize_ps_3: vBytesPerSecondByFeeRate['3'] || 0,
|
||||
vsize_ps_4: vBytesPerSecondByFeeRate['4'] || 0,
|
||||
vsize_ps_5: vBytesPerSecondByFeeRate['5'] || 0,
|
||||
vsize_ps_6: vBytesPerSecondByFeeRate['6'] || 0,
|
||||
vsize_ps_8: vBytesPerSecondByFeeRate['8'] || 0,
|
||||
vsize_ps_10: vBytesPerSecondByFeeRate['10'] || 0,
|
||||
vsize_ps_12: vBytesPerSecondByFeeRate['12'] || 0,
|
||||
vsize_ps_15: vBytesPerSecondByFeeRate['15'] || 0,
|
||||
vsize_ps_20: vBytesPerSecondByFeeRate['20'] || 0,
|
||||
vsize_ps_30: vBytesPerSecondByFeeRate['30'] || 0,
|
||||
vsize_ps_40: vBytesPerSecondByFeeRate['40'] || 0,
|
||||
vsize_ps_50: vBytesPerSecondByFeeRate['50'] || 0,
|
||||
vsize_ps_60: vBytesPerSecondByFeeRate['60'] || 0,
|
||||
vsize_ps_70: vBytesPerSecondByFeeRate['70'] || 0,
|
||||
vsize_ps_80: vBytesPerSecondByFeeRate['80'] || 0,
|
||||
vsize_ps_90: vBytesPerSecondByFeeRate['90'] || 0,
|
||||
vsize_ps_100: vBytesPerSecondByFeeRate['100'] || 0,
|
||||
vsize_ps_125: vBytesPerSecondByFeeRate['125'] || 0,
|
||||
vsize_ps_150: vBytesPerSecondByFeeRate['150'] || 0,
|
||||
vsize_ps_175: vBytesPerSecondByFeeRate['175'] || 0,
|
||||
vsize_ps_200: vBytesPerSecondByFeeRate['200'] || 0,
|
||||
vsize_ps_250: vBytesPerSecondByFeeRate['250'] || 0,
|
||||
vsize_ps_300: vBytesPerSecondByFeeRate['300'] || 0,
|
||||
vsize_ps_350: vBytesPerSecondByFeeRate['350'] || 0,
|
||||
vsize_ps_400: vBytesPerSecondByFeeRate['400'] || 0,
|
||||
vsize_ps_500: vBytesPerSecondByFeeRate['500'] || 0,
|
||||
vsize_ps_600: vBytesPerSecondByFeeRate['600'] || 0,
|
||||
vsize_ps_700: vBytesPerSecondByFeeRate['700'] || 0,
|
||||
vsize_ps_800: vBytesPerSecondByFeeRate['800'] || 0,
|
||||
vsize_ps_900: vBytesPerSecondByFeeRate['900'] || 0,
|
||||
vsize_ps_1000: vBytesPerSecondByFeeRate['1000'] || 0,
|
||||
vsize_ps_1200: vBytesPerSecondByFeeRate['1200'] || 0,
|
||||
vsize_ps_1400: vBytesPerSecondByFeeRate['1400'] || 0,
|
||||
vsize_ps_1600: vBytesPerSecondByFeeRate['1600'] || 0,
|
||||
vsize_ps_1800: vBytesPerSecondByFeeRate['1800'] || 0,
|
||||
vsize_ps_2000: vBytesPerSecondByFeeRate['2000'] || 0,
|
||||
});
|
||||
|
||||
if (this.newStatisticsEntryCallback && insertId) {
|
||||
|
||||
@@ -338,6 +338,87 @@ class TransactionUtils {
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
return witness[positionOfScript];
|
||||
}
|
||||
|
||||
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||
// (i.e. the most likely prioritizations and deprioritizations)
|
||||
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||
// find the longest increasing subsequence of transactions
|
||||
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||
// should be O(n log n)
|
||||
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||
if (X.length < 2) {
|
||||
return { prioritized: [], deprioritized: [] };
|
||||
}
|
||||
const N = X.length;
|
||||
const P: number[] = new Array(N);
|
||||
const M: number[] = new Array(N + 1);
|
||||
M[0] = -1; // undefined so can be set to any value
|
||||
|
||||
let L = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
// Binary search for the smallest positive l ≤ L
|
||||
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||
let lo = 1;
|
||||
let hi = L + 1;
|
||||
while (lo < hi) {
|
||||
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||
if (X[M[mid]].rate > X[i].rate) {
|
||||
hi = mid;
|
||||
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// After searching, lo == hi is 1 greater than the
|
||||
// length of the longest prefix of X[i]
|
||||
const newL = lo;
|
||||
|
||||
// The predecessor of X[i] is the last index of
|
||||
// the subsequence of length newL-1
|
||||
P[i] = M[newL - 1];
|
||||
M[newL] = i;
|
||||
|
||||
if (newL > L) {
|
||||
// If we found a subsequence longer than any we've
|
||||
// found yet, update L
|
||||
L = newL;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the longest increasing subsequence
|
||||
// It consists of the values of X at the L indices:
|
||||
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||
const LIS: any[] = new Array(L);
|
||||
let k = M[L];
|
||||
for (let j = L - 1; j >= 0; j--) {
|
||||
LIS[j] = X[k];
|
||||
k = P[k];
|
||||
}
|
||||
|
||||
const lisMap = new Map<string, number>();
|
||||
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||
|
||||
const prioritized: string[] = [];
|
||||
const deprioritized: string[] = [];
|
||||
|
||||
let lastRate = X[0].rate;
|
||||
|
||||
for (const tx of X) {
|
||||
if (lisMap.has(tx.txid)) {
|
||||
lastRate = tx.rate;
|
||||
} else {
|
||||
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||
// skip if the rate is almost the same as the previous transaction
|
||||
} else if (tx.rate <= lastRate) {
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
deprioritized.push(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { prioritized, deprioritized };
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
|
||||
@@ -439,6 +439,46 @@ export interface Statistic {
|
||||
vsize_1600: number;
|
||||
vsize_1800: number;
|
||||
vsize_2000: number;
|
||||
|
||||
vsize_ps_1: number;
|
||||
vsize_ps_2: number;
|
||||
vsize_ps_3: number;
|
||||
vsize_ps_4: number;
|
||||
vsize_ps_5: number;
|
||||
vsize_ps_6: number;
|
||||
vsize_ps_8: number;
|
||||
vsize_ps_10: number;
|
||||
vsize_ps_12: number;
|
||||
vsize_ps_15: number;
|
||||
vsize_ps_20: number;
|
||||
vsize_ps_30: number;
|
||||
vsize_ps_40: number;
|
||||
vsize_ps_50: number;
|
||||
vsize_ps_60: number;
|
||||
vsize_ps_70: number;
|
||||
vsize_ps_80: number;
|
||||
vsize_ps_90: number;
|
||||
vsize_ps_100: number;
|
||||
vsize_ps_125: number;
|
||||
vsize_ps_150: number;
|
||||
vsize_ps_175: number;
|
||||
vsize_ps_200: number;
|
||||
vsize_ps_250: number;
|
||||
vsize_ps_300: number;
|
||||
vsize_ps_350: number;
|
||||
vsize_ps_400: number;
|
||||
vsize_ps_500: number;
|
||||
vsize_ps_600: number;
|
||||
vsize_ps_700: number;
|
||||
vsize_ps_800: number;
|
||||
vsize_ps_900: number;
|
||||
vsize_ps_1000: number;
|
||||
vsize_ps_1200: number;
|
||||
vsize_ps_1400: number;
|
||||
vsize_ps_1600: number;
|
||||
vsize_ps_1800: number;
|
||||
vsize_ps_2000: number;
|
||||
|
||||
}
|
||||
|
||||
export interface OptimizedStatistic {
|
||||
@@ -449,6 +489,7 @@ export interface OptimizedStatistic {
|
||||
mempool_byte_weight: number;
|
||||
min_fee: number;
|
||||
vsizes: number[];
|
||||
vsizes_ps: number[];
|
||||
}
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
@@ -481,6 +522,7 @@ export interface WebsocketResponse {
|
||||
export interface VbytesPerSecond {
|
||||
unixTime: number;
|
||||
vSize: number;
|
||||
effectiveFeePerVsize: number;
|
||||
}
|
||||
|
||||
export interface RequiredSpec { [name: string]: RequiredParams; }
|
||||
|
||||
@@ -1106,7 +1106,7 @@ class BlocksRepository {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||
summaryVersion = 1;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
|
||||
@@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
@@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
||||
|
||||
### 3. Run the Frontend
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||
|
||||
Install project dependencies and run the frontend server:
|
||||
|
||||
@@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
||||
|
||||
### 1. Build the Frontend
|
||||
|
||||
_Make sure to use Node.js 16.10 and npm 7._
|
||||
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||
|
||||
Build the frontend:
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"hr": {
|
||||
"translation": "src/locale/messages.hr.xlf",
|
||||
"baseHref": "/hr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"baseHref": "/ja/"
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"gitCommit": "abbc8a134",
|
||||
"lightning": false
|
||||
},
|
||||
|
||||
59
frontend/package-lock.json
generated
59
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.1",
|
||||
@@ -32,6 +32,7 @@
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cypress": "^13.14.0",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.5.0",
|
||||
"esbuild": "^0.23.0",
|
||||
@@ -42,7 +43,7 @@
|
||||
"rxjs": "~7.8.1",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"tslib": "~2.7.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -62,7 +63,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.13.0",
|
||||
"cypress": "^13.14.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
@@ -699,6 +700,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/@angular-devkit/build-webpack": {
|
||||
"version": "0.1703.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz",
|
||||
@@ -8040,13 +8046,13 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.13.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
|
||||
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
|
||||
"version": "13.14.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
|
||||
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
"@cypress/request": "^3.0.1",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
"@types/sizzle": "^2.3.2",
|
||||
@@ -8805,9 +8811,9 @@
|
||||
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
|
||||
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
@@ -16925,9 +16931,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
|
||||
},
|
||||
"node_modules/tuf-js": {
|
||||
"version": "2.2.0",
|
||||
@@ -18849,6 +18855,11 @@
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -24127,12 +24138,12 @@
|
||||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.13.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
|
||||
"integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
|
||||
"version": "13.14.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz",
|
||||
"integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
"@cypress/request": "^3.0.1",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
"@types/sizzle": "^2.3.2",
|
||||
@@ -24723,9 +24734,9 @@
|
||||
"integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg=="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
|
||||
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
|
||||
"version": "6.5.7",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
|
||||
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
|
||||
"requires": {
|
||||
"bn.js": "^4.11.9",
|
||||
"brorand": "^1.1.0",
|
||||
@@ -30763,9 +30774,9 @@
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
|
||||
},
|
||||
"tuf-js": {
|
||||
"version": "2.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -95,7 +95,7 @@
|
||||
"esbuild": "^0.23.0",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"tslib": "~2.7.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -115,7 +115,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.13.0",
|
||||
"cypress": "^13.14.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
||||
@@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
return;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||
@@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
||||
}
|
||||
}
|
||||
const opM = ops.pop();
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<span>Spiral</span>
|
||||
</a>
|
||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
|
||||
<defs>
|
||||
<style>
|
||||
.d {
|
||||
@@ -125,17 +125,14 @@
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
|
||||
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
|
||||
</svg>
|
||||
<span>Unchained</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||
<span>Bitkey</span>
|
||||
</a>
|
||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -150,7 +147,7 @@
|
||||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
<g clip-path="url(#clip0_2_14)">
|
||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||
@@ -191,6 +188,19 @@
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||
<rect style="fill: black" width="360" height="360" />
|
||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://leather.io/" target="_blank" title="Leather">
|
||||
<img class="image" src="/resources/profile/leather.svg" />
|
||||
<span>Leather</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -435,7 +445,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
@@ -158,9 +156,8 @@
|
||||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,3 +251,12 @@
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.enterprise-sponsor {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,17 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
|
||||
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||
const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0;
|
||||
// Find the first pool with at least 1% of the total network hashrate
|
||||
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
|
||||
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
|
||||
acceleratingPools.forEach((poolId, index) => {
|
||||
const pool = pools[poolId];
|
||||
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
|
||||
data.push(getDataItem(
|
||||
pool.lastEstimatedHashrate,
|
||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)),
|
||||
index >= firstSignificantPool
|
||||
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
|
||||
: 'white',
|
||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
}
|
||||
|
||||
handleVin() {
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
|
||||
if (address?.scripts.size) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
|
||||
@@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
|
||||
// initialize the scene without any entry transition
|
||||
setup(transactions: TransactionStripped[]): void {
|
||||
setup(transactions: TransactionStripped[], sort: boolean = false): void {
|
||||
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
||||
if (filtersAvailable !== this.filtersAvailable) {
|
||||
this.setFilterFlags();
|
||||
@@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.filtersAvailable = filtersAvailable;
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.setup(transactions);
|
||||
this.scene.setup(transactions, sort);
|
||||
this.readyNextFrame = true;
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
|
||||
@@ -88,16 +88,19 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
// set up the scene with an initial set of transactions, without any transition animation
|
||||
setup(txs: TransactionStripped[]) {
|
||||
setup(txs: TransactionStripped[], sort: boolean = false) {
|
||||
// clean up any old transactions
|
||||
Object.values(this.txs).forEach(tx => {
|
||||
tx.destroy();
|
||||
delete this.txs[tx.txid];
|
||||
});
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
txs.forEach(tx => {
|
||||
const txView = new TxView(tx, this);
|
||||
this.txs[tx.txid] = txView;
|
||||
let txViews = txs.map(tx => new TxView(tx, this));
|
||||
if (sort) {
|
||||
txViews = txViews.sort(feeRateDescending);
|
||||
}
|
||||
txViews.forEach(txView => {
|
||||
this.txs[txView.txid] = txView;
|
||||
this.place(txView);
|
||||
this.saveGridToScreenPosition(txView);
|
||||
this.applyTxUpdate(txView, {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped {
|
||||
flags: number;
|
||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
time?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
|
||||
@@ -142,6 +142,10 @@ export function defaultColorFunction(
|
||||
return auditColors.added_prioritized;
|
||||
case 'prioritized':
|
||||
return auditColors.prioritized;
|
||||
case 'added_deprioritized':
|
||||
return auditColors.added_prioritized;
|
||||
case 'deprioritized':
|
||||
return auditColors.prioritized;
|
||||
case 'selected':
|
||||
return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
|
||||
@@ -79,6 +79,11 @@
|
||||
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
|
||||
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Prioritized">Prioritized</span>
|
||||
</ng-container>
|
||||
<span *ngSwitchCase="'deprioritized'" class="badge badge-warning" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
|
||||
<ng-container *ngSwitchCase="'added_deprioritized'">
|
||||
<span class="badge badge-warning" i18n="tx-features.tag.added|Added">Added</span>
|
||||
<span class="badge badge-warning ml-1" i18n="tx-features.tag.prioritized|Deprioritized">Deprioritized</span>
|
||||
</ng-container>
|
||||
<span *ngSwitchCase="'selected'" class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span>
|
||||
<span *ngSwitchCase="'rbf'" class="badge badge-warning" i18n="tx-features.tag.conflict|Conflict">Conflict</span>
|
||||
<span *ngSwitchCase="'accelerated'" class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { PreloadService } from '../../services/preload.service';
|
||||
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isUnseen = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isDeprioritized = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
@@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.numUnexpected = 0;
|
||||
|
||||
if (blockAudit?.template) {
|
||||
// augment with locally calculated *de*prioritized transactions if possible
|
||||
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
|
||||
// but if the local calculation produces returns unexpected results, don't use it
|
||||
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
|
||||
for (const tx of prioritized) {
|
||||
if (!isPrioritized[tx] && !isAccelerated[tx]) {
|
||||
useLocalDeprioritized = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of blockAudit.template) {
|
||||
inTemplate[tx.txid] = true;
|
||||
if (tx.acc) {
|
||||
@@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
||||
for (const txid of blockAudit.prioritizedTxs) {
|
||||
isPrioritized[txid] = true;
|
||||
}
|
||||
if (useLocalDeprioritized) {
|
||||
for (const txid of deprioritized || []) {
|
||||
isDeprioritized[txid] = true;
|
||||
}
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
@@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
tx.status = 'prioritized';
|
||||
}
|
||||
} else if (isDeprioritized[tx.txid]) {
|
||||
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
|
||||
tx.status = 'added_deprioritized';
|
||||
} else {
|
||||
tx.status = 'deprioritized';
|
||||
}
|
||||
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
|
||||
<div class="faucet-container text-center">
|
||||
|
||||
|
||||
@if (txid) {
|
||||
<div class="alert alert-success w-100 text-truncate">
|
||||
<fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
|
||||
@@ -36,6 +36,13 @@
|
||||
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
|
||||
</div>
|
||||
}
|
||||
@else if (error === 'account_limited') {
|
||||
<div class="alert alert-mempool d-block text-center w-100">
|
||||
<div class="d-inline align-middle">
|
||||
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@else if (error) {
|
||||
<!-- User can request -->
|
||||
<app-mempool-error class="w-100" [error]="error"></app-mempool-error>
|
||||
@@ -81,7 +88,7 @@
|
||||
}
|
||||
|
||||
<!-- Send back coins -->
|
||||
@if (status?.address) {
|
||||
@if (status?.address) {
|
||||
<div class="mt-4 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
|
||||
error: string = '';
|
||||
user: any = undefined;
|
||||
txid: string = '';
|
||||
|
||||
|
||||
faucetStatusSubscription: Subscription;
|
||||
status: {
|
||||
min: number; // minimum amount to request at once (in sats)
|
||||
|
||||
@@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
lastBlockHeight: number;
|
||||
blockIndex: number;
|
||||
isLoading$ = new BehaviorSubject<boolean>(true);
|
||||
isLoading$ = new BehaviorSubject<boolean>(false);
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
chainDirection: string = 'right';
|
||||
@@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
}
|
||||
}
|
||||
this.updateBlock({
|
||||
block: this.blockIndex,
|
||||
removed,
|
||||
changed,
|
||||
added
|
||||
@@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
||||
}
|
||||
this.isLoading$.next(true);
|
||||
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
|
||||
if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) {
|
||||
this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions));
|
||||
} else {
|
||||
this.isLoading$.next(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
this.isLoading$.next(false);
|
||||
}
|
||||
|
||||
resumeBlock(transactionsStripped: TransactionStripped[]): void {
|
||||
if (this.blockGraph) {
|
||||
this.firstLoad = false;
|
||||
this.blockGraph.setup(transactionsStripped, true);
|
||||
this.blockIndex = this.index;
|
||||
this.isLoading$.next(false);
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
this.resumeBlock(transactionsStripped);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||
if (!event.keyModifier) {
|
||||
|
||||
@@ -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$;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
if (state.mempoolPosition) {
|
||||
this.txPosition = state.mempoolPosition;
|
||||
if (this.txPosition.accelerated && !oldTxPosition.accelerated) {
|
||||
if (this.txPosition.accelerated && !oldTxPosition?.accelerated) {
|
||||
this.acceleratingArrow = true;
|
||||
setTimeout(() => {
|
||||
this.acceleratingArrow = false;
|
||||
|
||||
@@ -293,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
@@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
|
||||
checkAccelerationEligibility() {
|
||||
if (this.tx) {
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||
const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n;
|
||||
const highSigop = (this.tx.sigops * 20) > this.tx.weight;
|
||||
this.eligibleForAcceleration = !replaceableInputs && !highSigop;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 3, 2024</h6>
|
||||
<h6>Updated: August 19, 2024</h6>
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
@@ -100,11 +100,26 @@
|
||||
<p>The Mempool Accelerator Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-research.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool research Logo</p>
|
||||
<br><br>
|
||||
|
||||
<app-svg-images name="goggles" height="96px"></app-svg-images>
|
||||
<br><br>
|
||||
<p>The Mempool Goggles Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-transaction.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool transaction Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-block-visualization.png" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool block visualization Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool Blocks Logo</p>
|
||||
|
||||
@@ -606,16 +606,11 @@
|
||||
@if (!isLoadingTx) {
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
@if (accelerationInfo?.bidBoost) {
|
||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo.bidBoost | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + accelerationInfo.bidBoost"></app-fiat></span>
|
||||
} @else if (tx.feeDelta && !accelerationInfo) {
|
||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + tx.feeDelta"></app-fiat></span>
|
||||
} @else {
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee"></app-fiat></span>
|
||||
}
|
||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sat|sat">sat</span>
|
||||
}
|
||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { StorageService } from '../../services/storage.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { getTransactionFlags, getUnacceleratedFeeRate } from '../../shared/transaction.utils';
|
||||
import { Filter, TransactionFlags, toFilters } from '../../shared/filters.utils';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition, OptimizedMempoolStats } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
@@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
firstLoad = true;
|
||||
waitingForAccelerationInfo: boolean = false;
|
||||
isLoadingFirstSeen = false;
|
||||
mempoolStats: OptimizedMempoolStats[] = null;
|
||||
|
||||
featuresEnabled: boolean;
|
||||
segwitEnabled: boolean;
|
||||
@@ -196,7 +197,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => {
|
||||
this.network = network;
|
||||
@@ -358,12 +359,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}),
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
if (acceleration.txid === this.txId) {
|
||||
if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') {
|
||||
if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) {
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
} else {
|
||||
this.tx.feeDelta = undefined;
|
||||
}
|
||||
}
|
||||
this.waitingForAccelerationInfo = false;
|
||||
this.setIsAccelerated();
|
||||
}
|
||||
@@ -763,8 +770,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||
this.isAccelerated$,
|
||||
this.txChanged$,
|
||||
this.apiService.list2HStatistics$(),
|
||||
this.stateService.live2Chart$.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
map(([position, mempoolBlocks, da, isAccelerated]) => {
|
||||
map(([position, mempoolBlocks, da, isAccelerated, _, mempoolStats, mempoolStat]) => {
|
||||
|
||||
if (this.mempoolStats === null) {
|
||||
this.mempoolStats = mempoolStats;
|
||||
}
|
||||
|
||||
if (this.mempoolStats.length && mempoolStat && this.mempoolStats[0].added !== mempoolStat.added) {
|
||||
this.mempoolStats.pop();
|
||||
this.mempoolStats.unshift(mempoolStat);
|
||||
}
|
||||
|
||||
return this.etaService.calculateETA(
|
||||
this.network,
|
||||
this.tx,
|
||||
@@ -774,6 +793,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.miningStats,
|
||||
isAccelerated,
|
||||
this.accelerationPositions,
|
||||
this.mempoolStats,
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -895,7 +915,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
||||
this.checkAccelerationEligibility();
|
||||
} else {
|
||||
@@ -971,6 +991,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.isAcceleration = false;
|
||||
this.isAccelerated$.next(this.isAcceleration);
|
||||
this.eligibleForAcceleration = false;
|
||||
this.mempoolStats = null;
|
||||
this.leaveTransaction();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface OptimizedMempoolStats {
|
||||
total_fee: number;
|
||||
mempool_byte_weight: number;
|
||||
vsizes: number[];
|
||||
vsizes_ps: number[];
|
||||
}
|
||||
|
||||
interface Ancestor {
|
||||
@@ -239,7 +240,7 @@ export interface TransactionStripped {
|
||||
acc?: boolean;
|
||||
flags?: number | null;
|
||||
time?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
}
|
||||
|
||||
export interface MempoolBlockDelta {
|
||||
block: number;
|
||||
added: TransactionStripped[];
|
||||
removed: string[];
|
||||
changed: { txid: string, rate: number, flags: number, acc: boolean }[];
|
||||
}
|
||||
export interface MempoolBlockState {
|
||||
block: number;
|
||||
transactions: TransactionStripped[];
|
||||
}
|
||||
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../interfaces/node-api.interface';
|
||||
import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, SinglePoolStats } from '../interfaces/node-api.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { MempoolBlock } from '../interfaces/websocket.interface';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
@@ -7,6 +7,7 @@ import { MiningService, MiningStats } from './mining.service';
|
||||
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
|
||||
import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs';
|
||||
import { feeLevels } from '../app.constants';
|
||||
|
||||
export interface ETA {
|
||||
now: number, // time at which calculation performed
|
||||
@@ -113,6 +114,7 @@ export class EtaService {
|
||||
miningStats: MiningStats,
|
||||
isAccelerated: boolean,
|
||||
accelerationPositions: AccelerationPosition[],
|
||||
mempoolStats: OptimizedMempoolStats[] = [],
|
||||
): ETA | null {
|
||||
// return this.calculateETA(tx, this.accelerationPositions, position, mempoolBlocks, da, isAccelerated)
|
||||
if (!tx || !mempoolBlocks) {
|
||||
@@ -143,7 +145,17 @@ export class EtaService {
|
||||
|
||||
if (!isAccelerated) {
|
||||
const blocks = mempoolPosition.block + 1;
|
||||
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
|
||||
|
||||
const vsizeAhead = mempoolPosition.vsize + this.stateService.blockVSize * mempoolPosition.block;
|
||||
const incomingVsizePerBlock = this.estimateVsizePerSecond(tx, mempoolStats) * da.adjustedTimeAvg / 1000;
|
||||
const vsizeConsumedPerBlock = Math.max(
|
||||
this.stateService.blockVSize - incomingVsizePerBlock,
|
||||
0.05 * this.stateService.blockVSize // So that we don't return infinite ETA
|
||||
)
|
||||
|
||||
const blocksUntilMined = Math.ceil(vsizeAhead / vsizeConsumedPerBlock);
|
||||
const wait = blocksUntilMined * da.adjustedTimeAvg;
|
||||
|
||||
return {
|
||||
now,
|
||||
time: wait + now + da.timeOffset,
|
||||
@@ -279,4 +291,46 @@ export class EtaService {
|
||||
return tx.fee / (tx.weight / 4);
|
||||
|
||||
}
|
||||
|
||||
estimateVsizePerSecond(tx: Transaction, mempoolStats: OptimizedMempoolStats[], timeWindow: number = 15 * 60 * 1000): number {
|
||||
const nowMinusTimeSpan = (new Date().getTime() - timeWindow) / 1000;
|
||||
const vsizeAboveTransaction = mempoolStats
|
||||
// Remove datapoints older than now - timeWindow
|
||||
.filter(stat => stat.added > nowMinusTimeSpan)
|
||||
// Remove datapoints less than 45 seconds apart from the previous one
|
||||
.filter((el, i, arr) => {
|
||||
if (i === 0) {
|
||||
return true;
|
||||
}
|
||||
return arr[i - 1].added - el.added > 45;
|
||||
})
|
||||
// For each datapoint, compute the total vsize of transactions with higher fee rate
|
||||
.map(stat => {
|
||||
let vsizeAbove = 0;
|
||||
for (let i = feeLevels.length - 1; i >= 0; i--) {
|
||||
if (feeLevels[i] > tx.effectiveFeePerVsize) {
|
||||
vsizeAbove += stat.vsizes_ps[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return vsizeAbove;
|
||||
});
|
||||
|
||||
// vsizeAboveTransaction is a temporal series of past vsize values above the transaction's fee rate
|
||||
// From this array we need to estimate the future vsize per second
|
||||
// Naive first approach: take the median of the series
|
||||
if (!vsizeAboveTransaction.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sorted = Array.from(vsizeAboveTransaction).sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[middle - 1] + sorted[middle]) / 2;
|
||||
}
|
||||
|
||||
return sorted[middle];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool
|
||||
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { filter, map, scan, share, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||
import { ActiveFilter } from '../shared/filters.utils';
|
||||
@@ -131,6 +131,7 @@ export class StateService {
|
||||
latestBlockHeight = -1;
|
||||
blocks: BlockExtended[] = [];
|
||||
mempoolSequence: number;
|
||||
mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} };
|
||||
|
||||
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
@@ -143,7 +144,7 @@ export class StateService {
|
||||
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
|
||||
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
|
||||
accelerations$ = new Subject<AccelerationDelta>();
|
||||
liveAccelerations$: Observable<Acceleration[]>;
|
||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||
@@ -231,29 +232,40 @@ export class StateService {
|
||||
}
|
||||
});
|
||||
|
||||
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => {
|
||||
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => {
|
||||
if (isMempoolState(change)) {
|
||||
const txMap = {};
|
||||
change.transactions.forEach(tx => {
|
||||
txMap[tx.txid] = tx;
|
||||
});
|
||||
return txMap;
|
||||
this.mempoolBlockState = {
|
||||
block: change.block,
|
||||
transactions: txMap
|
||||
};
|
||||
return this.mempoolBlockState;
|
||||
} else {
|
||||
change.added.forEach(tx => {
|
||||
transactions[tx.txid] = tx;
|
||||
acc.transactions[tx.txid] = tx;
|
||||
});
|
||||
change.removed.forEach(txid => {
|
||||
delete transactions[txid];
|
||||
delete acc.transactions[txid];
|
||||
});
|
||||
change.changed.forEach(tx => {
|
||||
if (transactions[tx.txid]) {
|
||||
transactions[tx.txid].rate = tx.rate;
|
||||
transactions[tx.txid].acc = tx.acc;
|
||||
if (acc.transactions[tx.txid]) {
|
||||
acc.transactions[tx.txid].rate = tx.rate;
|
||||
acc.transactions[tx.txid].acc = tx.acc;
|
||||
}
|
||||
});
|
||||
return transactions;
|
||||
this.mempoolBlockState = {
|
||||
block: change.block,
|
||||
transactions: acc.transactions
|
||||
};
|
||||
return this.mempoolBlockState;
|
||||
}
|
||||
}, {}));
|
||||
}, {}),
|
||||
share()
|
||||
);
|
||||
this.liveMempoolBlockTransactions$.subscribe();
|
||||
|
||||
// Emits the full list of pending accelerations each time it changes
|
||||
this.liveAccelerations$ = this.accelerations$.pipe(
|
||||
|
||||
@@ -35,6 +35,7 @@ export class WebsocketService {
|
||||
private isTrackingAddresses: string[] | false = false;
|
||||
private isTrackingAccelerations: boolean = false;
|
||||
private trackingMempoolBlock: number;
|
||||
private stoppingTrackMempoolBlock: any | null = null;
|
||||
private latestGitCommit = '';
|
||||
private onlineCheckTimeout: number;
|
||||
private onlineCheckTimeoutTwo: number;
|
||||
@@ -203,19 +204,31 @@ export class WebsocketService {
|
||||
this.websocketSubject.next({ 'track-asset': 'stop' });
|
||||
}
|
||||
|
||||
startTrackMempoolBlock(block: number, force: boolean = false) {
|
||||
startTrackMempoolBlock(block: number, force: boolean = false): boolean {
|
||||
if (this.stoppingTrackMempoolBlock) {
|
||||
clearTimeout(this.stoppingTrackMempoolBlock);
|
||||
}
|
||||
// skip duplicate tracking requests
|
||||
if (force || this.trackingMempoolBlock !== block) {
|
||||
this.websocketSubject.next({ 'track-mempool-block': block });
|
||||
this.isTrackingMempoolBlock = true;
|
||||
this.trackingMempoolBlock = block;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
stopTrackMempoolBlock() {
|
||||
this.websocketSubject.next({ 'track-mempool-block': -1 });
|
||||
stopTrackMempoolBlock(): void {
|
||||
if (this.stoppingTrackMempoolBlock) {
|
||||
clearTimeout(this.stoppingTrackMempoolBlock);
|
||||
}
|
||||
this.isTrackingMempoolBlock = false;
|
||||
this.trackingMempoolBlock = null;
|
||||
this.stoppingTrackMempoolBlock = setTimeout(() => {
|
||||
this.stoppingTrackMempoolBlock = null;
|
||||
this.websocketSubject.next({ 'track-mempool-block': -1 });
|
||||
this.trackingMempoolBlock = null;
|
||||
this.stateService.mempoolBlockState = null;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
startTrackRbf(mode: 'all' | 'fullRbf') {
|
||||
@@ -424,6 +437,7 @@ export class WebsocketService {
|
||||
if (response['projected-block-transactions'].blockTransactions) {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockUpdate$.next({
|
||||
block: this.trackingMempoolBlock,
|
||||
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
|
||||
});
|
||||
} else if (response['projected-block-transactions'].delta) {
|
||||
@@ -432,7 +446,7 @@ export class WebsocketService {
|
||||
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
||||
} else {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
|
||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export type AddressType = 'fee'
|
||||
| 'v0_p2wsh'
|
||||
| 'v1_p2tr'
|
||||
| 'confidential'
|
||||
| 'anchor'
|
||||
| 'unknown'
|
||||
|
||||
const ADDRESS_PREFIXES = {
|
||||
@@ -188,6 +189,12 @@ export class AddressTypeInfo {
|
||||
const v = vin[0];
|
||||
this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm));
|
||||
}
|
||||
} else if (this.type === 'unknown') {
|
||||
for (const v of vin) {
|
||||
if (v.prevout?.scriptpubkey === '51024e73') {
|
||||
this.type = 'anchor';
|
||||
}
|
||||
}
|
||||
}
|
||||
// and there's nothing more to learn from processing inputs for other types
|
||||
}
|
||||
@@ -197,6 +204,10 @@ export class AddressTypeInfo {
|
||||
if (!this.scripts.size) {
|
||||
this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm));
|
||||
}
|
||||
} else if (this.type === 'unknown') {
|
||||
if (output.scriptpubkey === '51024e73') {
|
||||
this.type = 'anchor';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -170,8 +170,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped {
|
||||
};
|
||||
}
|
||||
|
||||
export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
|
||||
export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta {
|
||||
return {
|
||||
block,
|
||||
added: delta.added.map(uncompressTx),
|
||||
removed: delta.removed,
|
||||
changed: delta.changed.map(tx => ({
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
@case ('multisig') {
|
||||
<span i18n="address.bare-multisig">bare multisig</span>
|
||||
}
|
||||
@case ('anchor') {
|
||||
<span>anchor</span>
|
||||
}
|
||||
@case (null) {
|
||||
<span>unknown</span>
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
|
||||
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
|
||||
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
|
||||
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
|
||||
anchor: () => ({ type: 'anchor', label: 'anchor' }),
|
||||
};
|
||||
|
||||
export class ScriptInfo {
|
||||
@@ -266,7 +267,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
|
||||
if (!opN) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@@ -286,7 +287,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
|
||||
if (!opM) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark} from '@fortawesome/free-solid-svg-icons';
|
||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { MenuComponent } from '../components/menu/menu.component';
|
||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
|
||||
@@ -440,5 +440,6 @@ export class SharedModule {
|
||||
library.addIcons(faFaucetDrip);
|
||||
library.addIcons(faTimeline);
|
||||
library.addIcons(faCircleXmark);
|
||||
library.addIcons(faCalendarCheck);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TransactionFlags } from './filters.utils';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface';
|
||||
import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { StateService } from '../services/state.service';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean {
|
||||
*
|
||||
* returns true early if any standardness rule is violated, otherwise false
|
||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||
*
|
||||
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||
* For now, just pull out individual rules into versioned functions where necessary.
|
||||
*/
|
||||
export function isNonStandard(tx: Transaction): boolean {
|
||||
export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean {
|
||||
// version
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (isNonStandardVersion(tx, height, network)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (isNonStandardAnchor(tx, height, network)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Individual versioned standardness rules
|
||||
|
||||
const V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean {
|
||||
let TX_MAX_STANDARD_VERSION = 3;
|
||||
if (
|
||||
height != null
|
||||
&& network != null
|
||||
&& V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
&& height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
) {
|
||||
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
TX_MAX_STANDARD_VERSION = 2;
|
||||
}
|
||||
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||
'testnet4': 42_000,
|
||||
'testnet': 2_900_000,
|
||||
'signet': 211_000,
|
||||
'': 863_500,
|
||||
};
|
||||
function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean {
|
||||
if (
|
||||
height != null
|
||||
&& network != null
|
||||
&& ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
&& height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network]
|
||||
) {
|
||||
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
|
||||
// followed by a data push between 2 and 40 bytes.
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
|
||||
@@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean {
|
||||
].includes(pubkey);
|
||||
}
|
||||
|
||||
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint {
|
||||
export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
|
||||
flags |= TransactionFlags.batch_payout;
|
||||
}
|
||||
|
||||
if (isNonStandard(tx)) {
|
||||
if (isNonStandard(tx, height, network)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
@@ -458,4 +508,83 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean):
|
||||
} else {
|
||||
return tx.effectiveFeePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
export function identifyPrioritizedTransactions(transactions: TransactionStripped[]): { prioritized: string[], deprioritized: string[] } {
|
||||
// find the longest increasing subsequence of transactions
|
||||
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||
// should be O(n log n)
|
||||
const X = transactions.slice(1).reverse(); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||
if (X.length < 2) {
|
||||
return { prioritized: [], deprioritized: [] };
|
||||
}
|
||||
const N = X.length;
|
||||
const P: number[] = new Array(N);
|
||||
const M: number[] = new Array(N + 1);
|
||||
M[0] = -1; // undefined so can be set to any value
|
||||
|
||||
let L = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
// Binary search for the smallest positive l ≤ L
|
||||
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||
let lo = 1;
|
||||
let hi = L + 1;
|
||||
while (lo < hi) {
|
||||
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||
if (X[M[mid]].rate > X[i].rate) {
|
||||
hi = mid;
|
||||
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// After searching, lo == hi is 1 greater than the
|
||||
// length of the longest prefix of X[i]
|
||||
const newL = lo;
|
||||
|
||||
// The predecessor of X[i] is the last index of
|
||||
// the subsequence of length newL-1
|
||||
P[i] = M[newL - 1];
|
||||
M[newL] = i;
|
||||
|
||||
if (newL > L) {
|
||||
// If we found a subsequence longer than any we've
|
||||
// found yet, update L
|
||||
L = newL;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the longest increasing subsequence
|
||||
// It consists of the values of X at the L indices:
|
||||
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||
const LIS: TransactionStripped[] = new Array(L);
|
||||
let k = M[L];
|
||||
for (let j = L - 1; j >= 0; j--) {
|
||||
LIS[j] = X[k];
|
||||
k = P[k];
|
||||
}
|
||||
|
||||
const lisMap = new Map<string, number>();
|
||||
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||
|
||||
const prioritized: string[] = [];
|
||||
const deprioritized: string[] = [];
|
||||
|
||||
let lastRate = 0;
|
||||
|
||||
for (const tx of X) {
|
||||
if (lisMap.has(tx.txid)) {
|
||||
lastRate = tx.rate;
|
||||
} else {
|
||||
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||
// skip if the rate is almost the same as the previous transaction
|
||||
} else if (tx.rate <= lastRate) {
|
||||
prioritized.push(tx.txid);
|
||||
} else {
|
||||
deprioritized.push(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { prioritized, deprioritized };
|
||||
}
|
||||
@@ -1567,7 +1567,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
|
||||
<source>Total Bid Boost</source>
|
||||
<target>Ukupno povećanje ponude</target>
|
||||
<target>Total Bid Boost</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
@@ -1969,7 +1969,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="841f2a74ae5095e6e37f5749f3cc1851cf36a420" datatype="html">
|
||||
<source>Avg Max Bid</source>
|
||||
<target>Prosječna maks. ponuda</target>
|
||||
<target>Prosj. max ponuda</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/acceleration/pending-stats/pending-stats.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
@@ -3622,7 +3622,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="14779b0ce4cbc4d975a35a8fe074426228a324f3" datatype="html">
|
||||
<source><x id="INTERPOLATION" equiv-text="transactions</ng-template> </h2> <ngb-pagination class="pagination-container float-ri"/> transactions</source>
|
||||
<target><x id="INTERPOLATION" equiv-text="transactions</ng-template> </h2> <ngb-pagination class="pagination-container float-ri"/> transakcije</target>
|
||||
<target><x id="INTERPOLATION" equiv-text="transactions</ng-template> </h2> <ngb-pagination class="pagination-container float-ri"/> transakcija</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block/block-transactions.component.html</context>
|
||||
<context context-type="linenumber">5</context>
|
||||
@@ -4046,7 +4046,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="8a7b4bd44c0ac71b2e72de0398b303257f7d2f54" datatype="html">
|
||||
<source>Blocks</source>
|
||||
<target>Blokovi</target>
|
||||
<target>Blokova</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
@@ -4360,7 +4360,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="23c872b0336e20284724607f2887da39bd8142c3" datatype="html">
|
||||
<source>Previous fee</source>
|
||||
<target>Prethodna naknada</target>
|
||||
<target>Preth. naknada</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/custom-dashboard/custom-dashboard.component.html</context>
|
||||
<context context-type="linenumber">107</context>
|
||||
@@ -4594,7 +4594,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="1bb6965f8e1bbe40c076528ffd841da86f57f119" datatype="html">
|
||||
<source><x id="INTERPOLATION" equiv-text="<span class="shared-block">blocks</span></ng-template> <ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="shared-block">"/>blocks<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source>
|
||||
<target><x id="INTERPOLATION" equiv-text="<span class="shared-block">blocks</span></ng-template> <ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="shared-block">"/>blokovi<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></target>
|
||||
<target><x id="INTERPOLATION" equiv-text="<span class="shared-block">blocks</span></ng-template> <ng-"/> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="shared-block">"/>blokova<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
|
||||
<context context-type="linenumber">10,11</context>
|
||||
@@ -4671,7 +4671,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="df71fa93f0503396ea2bb3ba5161323330314d6c" datatype="html">
|
||||
<source>Next Halving</source>
|
||||
<target>Sljedeće prepolovljenje</target>
|
||||
<target>Slj prepolovljenje</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/difficulty-mining/difficulty-mining.component.html</context>
|
||||
<context context-type="linenumber">47</context>
|
||||
@@ -5465,7 +5465,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="1a8246eba9a999ee881248c4767d63b875ef07fe" datatype="html">
|
||||
<source>blocks</source>
|
||||
<target>blokovi</target>
|
||||
<target>blokova</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html</context>
|
||||
<context context-type="linenumber">63</context>
|
||||
@@ -5885,7 +5885,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="9ef8b357c32266f8423e24bf654006d3aa8fcd0b" datatype="html">
|
||||
<source>Blocks (1w)</source>
|
||||
<target>Blokova (1w)</target>
|
||||
<target>Blokova (1 tj)</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context>
|
||||
<context context-type="linenumber">25</context>
|
||||
@@ -6165,7 +6165,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="3dc78651b2810cbb6e830fe7e57499d8cf6a8e4d" datatype="html">
|
||||
<source>Blocks (24h)</source>
|
||||
<target>Blokovi (24h)</target>
|
||||
<target>Blokova (24h)</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
|
||||
<context context-type="linenumber">120</context>
|
||||
@@ -6815,7 +6815,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="time-until" datatype="html">
|
||||
<source>In ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></source>
|
||||
<target>U ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></target>
|
||||
<target>Za ~<x id="DATE" equiv-text="dateStrings.i18nYear"/></target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
|
||||
<context context-type="linenumber">188</context>
|
||||
|
||||
@@ -510,7 +510,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="e4b2d9e6a2ab9e6ca34027ec03beaac42b7badd4" datatype="html">
|
||||
<source>sats</source>
|
||||
<target>sats</target>
|
||||
<target>sat</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
|
||||
<context context-type="linenumber">57</context>
|
||||
@@ -881,7 +881,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="65fd4251d8ddfe4017d4d83f8cec6f5a80d89289" datatype="html">
|
||||
<source>Pay</source>
|
||||
<target>Betale</target>
|
||||
<target>Betal</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/accelerate-checkout/accelerate-checkout.component.html</context>
|
||||
<context context-type="linenumber">378</context>
|
||||
@@ -4846,7 +4846,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="615ba6c4511a36f93c225c725935fdbf16f162a5" datatype="html">
|
||||
<source>Amount (sats)</source>
|
||||
<target>Beløp (sats)</target>
|
||||
<target>Beløp (sat)</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/faucet/faucet.component.html</context>
|
||||
<context context-type="linenumber">51</context>
|
||||
@@ -6442,7 +6442,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="31443c29cb161e8aa661eb5035f675746ef95b45" datatype="html">
|
||||
<source>sats/tx</source>
|
||||
<target>sats/tx</target>
|
||||
<target>sat/tx</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/reward-stats/reward-stats.component.html</context>
|
||||
<context context-type="linenumber">33</context>
|
||||
@@ -8145,7 +8145,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="6acd06bd5a3af583cd46c6d9f7954d7a2b44095e" datatype="html">
|
||||
<source>mSats</source>
|
||||
<target>mSats</target>
|
||||
<target>mSat</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/lightning/channel/channel-box/channel-box.component.html</context>
|
||||
<context context-type="linenumber">35</context>
|
||||
|
||||
BIN
frontend/src/resources/mempool-block-visualization.png
Normal file
BIN
frontend/src/resources/mempool-block-visualization.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/src/resources/mempool-research.png
Normal file
BIN
frontend/src/resources/mempool-research.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/src/resources/mempool-transaction.png
Normal file
BIN
frontend/src/resources/mempool-transaction.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
3
frontend/src/resources/profile/bitkey.svg
Normal file
3
frontend/src/resources/profile/bitkey.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="212" height="450" viewBox="0 0 212 450" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.005 2.14659C109.052 -0.71553 102.948 -0.715531 97.9948 2.14659L7.99481 54.1531C3.04732 57.012 0 62.2924 0 68.0065V172.043C0 177.758 3.04733 183.038 7.99482 185.897L69.0247 221.163C73.9722 224.022 77.0195 229.302 77.0195 235.016V433.998V437.998C77.0195 444.625 82.3921 449.998 89.0195 449.998H93.0195H118.981H123.031C129.658 449.998 135.031 444.625 135.031 437.998V233.751C135.444 228.531 138.395 223.809 142.975 221.163L204.005 185.897C208.953 183.038 212 177.758 212 172.043V68.0065C212 62.2924 208.953 57.012 204.005 54.1531L114.005 2.14659ZM112.07 68.1162C108.334 65.938 103.716 65.938 99.9803 68.1162L63.9718 89.1129C60.2841 91.2631 58.0164 95.2105 58.0164 99.4793V141.68C58.0164 145.948 60.2841 149.896 63.9718 152.046L99.9803 173.043C103.716 175.221 108.334 175.221 112.07 173.043L148.078 152.046C151.766 149.896 154.034 145.948 154.034 141.68V99.4793C154.034 95.2105 151.766 91.2631 148.078 89.1129L112.07 68.1162ZM172.046 338.527C164.047 333.861 154 339.631 154 348.892L154 356L154 412V419.108C154 428.369 164.047 434.14 172.046 429.473L196.046 415.473C199.733 413.322 202 409.376 202 405.108V362.892C202 358.624 199.733 354.678 196.046 352.527L172.046 338.527Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
frontend/src/resources/profile/leather.svg
Normal file
3
frontend/src/resources/profile/leather.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="387" height="377" viewBox="-58.05 -56.55 503.1 490.1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M249.327 137.966C283.745 132.618 333.52 96.2553 333.52 67.9135C333.52 59.3574 326.637 53.4752 316.576 53.4752C297.513 53.4752 265.212 82.3518 249.327 137.966ZM89.9409 278.606C44.9317 278.606 41.225 323.525 86.2343 323.525C106.356 323.525 130.714 315.504 143.422 301.065C124.889 285.023 109.533 278.606 89.9409 278.606ZM376.411 259.355C379.059 334.755 340.934 377 276.332 377C238.207 377 219.144 362.562 178.371 335.824C157.19 359.353 116.946 377 83.5867 377C-31.3192 377 -26.5536 229.943 90.4704 229.943C114.828 229.943 135.48 236.36 161.956 252.938L179.43 191.441C107.415 171.655 71.4077 116.041 106.886 36.3631H164.074C132.303 89.3035 154.013 133.153 194.256 137.966C215.967 60.4269 262.565 0 324.518 0C359.467 0 387.002 22.9943 387.002 64.705C387.002 131.549 300.161 186.094 234.5 191.441L207.494 287.162C238.207 322.99 323.459 357.749 323.459 259.355H376.411Z" fill="#F5F1ED"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1011 B |
@@ -84,11 +84,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
|
||||
|
||||
### Node.js + npm
|
||||
|
||||
Build Node.js v16.16.0 and npm v8 from source using `nvm`:
|
||||
Build Node.js v20.17.0 and npm v9 from source using `nvm`:
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | zsh
|
||||
source $HOME/.zshrc
|
||||
nvm install v16.16.0 --shared-zlib
|
||||
nvm install v20.17.0 --shared-zlib
|
||||
nvm alias default node
|
||||
```
|
||||
|
||||
|
||||
@@ -392,9 +392,9 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba
|
||||
# packages needed for mempool ecosystem
|
||||
FREEBSD_PKG=()
|
||||
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
|
||||
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4)
|
||||
FREEBSD_PKG+=(openssh-portable py311-pip rust llvm18 jq base64 libzmq4)
|
||||
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
|
||||
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase)
|
||||
FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server)
|
||||
FREEBSD_PKG+=(geoipupdate redis)
|
||||
|
||||
FREEBSD_UNFURL_PKG=()
|
||||
|
||||
@@ -40,7 +40,7 @@ update_repo()
|
||||
git fetch origin || exit 1
|
||||
for remote in origin;do
|
||||
git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1
|
||||
git fetch "${remote}" || exit 1
|
||||
git fetch "${remote}" --tags || exit 1
|
||||
done
|
||||
|
||||
if [ $(git tag -l "${REF}") ];then
|
||||
|
||||
4
unfurler/package-lock.json
generated
4
unfurler/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-unfurl",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.0.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.11.41",
|
||||
"ejs": "^3.1.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "3.0.0-beta",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Renderer for mempool open graph link preview images",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user