Compare commits
25 Commits
v3.0.1
...
natsoni/bl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b3e52436 | ||
|
|
d04e5128ba | ||
|
|
e3c3f31ddb | ||
|
|
70d1f52268 | ||
|
|
e44f30d7a7 | ||
|
|
099d84a395 | ||
|
|
12285465d9 | ||
|
|
3bea10ea35 | ||
|
|
1ea45e9e96 | ||
|
|
8c2d0e1d6c | ||
|
|
009fba3dd5 | ||
|
|
a0fc4861d4 | ||
|
|
62085581dd | ||
|
|
05efa8c300 | ||
|
|
eee99a6407 | ||
|
|
98cee4a6cd | ||
|
|
0302999806 | ||
|
|
1876d67e74 | ||
|
|
c0bb75e5b1 | ||
|
|
4059a902a1 | ||
|
|
4cc19a7235 | ||
|
|
c874d642c5 | ||
|
|
c9171224e1 | ||
|
|
248cef7718 | ||
|
|
104c7f4285 |
@@ -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",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "3.0.0",
|
||||
"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",
|
||||
"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 = 82;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -700,6 +700,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(81);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||
await this.$fixBadV1AuditBlocks();
|
||||
await this.updateToSchemaVersion(82);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1314,6 +1319,28 @@ class DatabaseMigration {
|
||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||
}
|
||||
}
|
||||
|
||||
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||
const badBlocks = [
|
||||
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||
];
|
||||
|
||||
for (const hash of badBlocks) {
|
||||
try {
|
||||
await this.$executeQuery(`
|
||||
UPDATE blocks_audits
|
||||
SET prioritized_txs = '[]'
|
||||
WHERE hash = '${hash}'
|
||||
`, true);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseMigration();
|
||||
|
||||
@@ -30,6 +30,7 @@ class MiningRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustment/:height', this.$getDifficultyAdjustment)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlocksHealth)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores', this.$getBlockAuditScores)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/scores/:height', this.$getBlockAuditScores)
|
||||
@@ -297,6 +298,18 @@ class MiningRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getDifficultyAdjustment(req: Request, res: Response) {
|
||||
try {
|
||||
const adjustment = await DifficultyAdjustmentsRepository.$getAdjustmentAtHeight(parseInt(req.params.height, 10));
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
res.json(adjustment);
|
||||
} catch (e) {
|
||||
res.status(e instanceof Error && e.message === 'not found' ? 204 : 500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getRewardStats(req: Request, res: Response) {
|
||||
try {
|
||||
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -88,6 +88,22 @@ class DifficultyAdjustmentsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAdjustmentAtHeight(height: number): Promise<IndexedDifficultyAdjustment> {
|
||||
try {
|
||||
if (isNaN(height)) {
|
||||
throw new Error(`argument must be a number`);
|
||||
}
|
||||
const [rows] = await DB.query(`SELECT * FROM difficulty_adjustments WHERE height = ?`, [height]);
|
||||
if (!rows[0]) {
|
||||
throw new Error(`not found`);
|
||||
}
|
||||
return rows[0] as IndexedDifficultyAdjustment;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot get difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAdjustmentsHeights(): Promise<number[]> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0",
|
||||
"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",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "3.0.0",
|
||||
"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",
|
||||
"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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -78,6 +78,25 @@
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
|
||||
<td i18n="mining.difficulty-adjustment">Adjustment</td>
|
||||
<td>
|
||||
<ng-container *ngIf="cacheService.daCache[block.height]?.adjustment > 0; else loadingAdjustment">
|
||||
<div [style.color]="cacheService.daCache[block.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[block.height].adjustment < 1 ? 'var(--red)' : '')">
|
||||
@if (cacheService.daCache[block.height].adjustment > 1) {
|
||||
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
} @else if (cacheService.daCache[block.height].adjustment < 1) {
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
}
|
||||
{{ (cacheService.daCache[block.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingAdjustment>
|
||||
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #skeletonRows>
|
||||
<tr>
|
||||
@@ -193,6 +212,10 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && block.height % 2016 === 0">
|
||||
<td i18n="block.difficulty">Difficulty</td>
|
||||
<td>{{ block.difficulty | amountShortener: 2 }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template #loadingRest>
|
||||
<tr>
|
||||
|
||||
@@ -280,3 +280,11 @@ h1 {
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
&.up {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { PreloadService } from '../../services/preload.service';
|
||||
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -98,7 +99,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private apiService: ApiService,
|
||||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
public cacheService: CacheService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private cd: ChangeDetectorRef,
|
||||
private preloadService: PreloadService,
|
||||
@@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isUnseen = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isDeprioritized = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
@@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.numUnexpected = 0;
|
||||
|
||||
if (blockAudit?.template) {
|
||||
// augment with locally calculated *de*prioritized transactions if possible
|
||||
const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions);
|
||||
// but if the local calculation produces returns unexpected results, don't use it
|
||||
let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1);
|
||||
for (const tx of prioritized) {
|
||||
if (!isPrioritized[tx] && !isAccelerated[tx]) {
|
||||
useLocalDeprioritized = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of blockAudit.template) {
|
||||
inTemplate[tx.txid] = true;
|
||||
if (tx.acc) {
|
||||
@@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
||||
for (const txid of blockAudit.prioritizedTxs) {
|
||||
isPrioritized[txid] = true;
|
||||
}
|
||||
if (useLocalDeprioritized) {
|
||||
for (const txid of deprioritized || []) {
|
||||
isDeprioritized[txid] = true;
|
||||
}
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
@@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
tx.status = 'prioritized';
|
||||
}
|
||||
} else if (isDeprioritized[tx.txid]) {
|
||||
if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) {
|
||||
tx.status = 'added_deprioritized';
|
||||
} else {
|
||||
tx.status = 'deprioritized';
|
||||
}
|
||||
} else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
|
||||
import { Observable, Subscription, delay, filter, of, retryWhen, switchMap, take, tap, throwError } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { Location } from '@angular/common';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { colorFromRetarget } from '../../shared/common.utils';
|
||||
|
||||
interface BlockchainBlock extends BlockExtended {
|
||||
placeholder?: boolean;
|
||||
@@ -77,6 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
public cacheService: CacheService,
|
||||
public apiService: ApiService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private location: Location,
|
||||
) {
|
||||
@@ -334,6 +337,31 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
||||
}
|
||||
|
||||
isDA(height: number): boolean {
|
||||
const isDA = height % 2016 === 0 && this.stateService.network === '';
|
||||
if (isDA && !this.cacheService.daCache[height]?.exact) {
|
||||
const estimatedAdjustment = this.cacheService.daCache[height]?.adjustment || 0;
|
||||
this.cacheService.daCache[height] = { adjustment: estimatedAdjustment, exact: true };
|
||||
this.apiService.getDifficultyAdjustmentByHeight$(height).pipe(
|
||||
switchMap(da => {
|
||||
const blocksAvailable = (this.height || this.chainTip) && this.blockStyles[(this.height || this.chainTip) - height];
|
||||
return blocksAvailable ? of(da) : throwError(() => new Error());
|
||||
}),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(1000),
|
||||
take(3)
|
||||
)
|
||||
),
|
||||
tap((da) => {
|
||||
this.cacheService.daCache[height] = { adjustment: da?.adjustment || 1, exact: true };
|
||||
this.blockStyles[(this.height || this.chainTip) - height].background = colorFromRetarget(da?.adjustment);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
return isDA;
|
||||
}
|
||||
|
||||
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
|
||||
if (!block || block.placeholder) {
|
||||
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
|
||||
@@ -349,7 +377,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
return {
|
||||
left: addLeft + this.blockOffset * index + 'px',
|
||||
background: `repeating-linear-gradient(
|
||||
background: this.isDA(block.height) ? colorFromRetarget(this.cacheService.daCache[block.height]?.adjustment || 1) :
|
||||
`repeating-linear-gradient(
|
||||
var(--secondary),
|
||||
var(--secondary) ${greenBackgroundHeight}%,
|
||||
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { selectPowerOfTen } from '../../bitcoin.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-difficulty-adjustments-table',
|
||||
@@ -27,7 +28,8 @@ export class DifficultyAdjustmentsTable implements OnInit {
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService
|
||||
public stateService: StateService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -52,6 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
|
||||
adjustment[2] / selectedPowerOfTen.divider,
|
||||
this.locale, `1.${decimals}-${decimals}`) + selectedPowerOfTen.unit
|
||||
});
|
||||
this.cacheService.daCache[adjustment[1]] = { adjustment: adjustment[3], exact: true };
|
||||
}
|
||||
this.isLoading = false;
|
||||
return tableData.slice(0, 6);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,6 +41,25 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="this.stateService.network === '' && mempoolBlock.height % 2016 === 0">
|
||||
<td i18n="mining.difficulty-adjustment">Adjustment</td>
|
||||
<td>
|
||||
<ng-container *ngIf="cacheService.daCache[mempoolBlock.height]?.adjustment > 0; else loadingAdjustment">
|
||||
<div [style.color]="cacheService.daCache[mempoolBlock.height].adjustment > 1 ? 'var(--green)' : (cacheService.daCache[mempoolBlock.height].adjustment < 1 ? 'var(--red)' : '')">
|
||||
@if (cacheService.daCache[mempoolBlock.height].adjustment > 1) {
|
||||
<fa-icon class="retarget-sign up" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||
} @else if (cacheService.daCache[mempoolBlock.height].adjustment < 1) {
|
||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||
}
|
||||
{{ (cacheService.daCache[mempoolBlock.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }}
|
||||
<span class="symbol">%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingAdjustment>
|
||||
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||
|
||||
@@ -36,3 +36,11 @@ h1 {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
&.up {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-block',
|
||||
@@ -30,6 +31,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
public cacheService: CacheService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
@@ -71,7 +73,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
|
||||
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions)));
|
||||
|
||||
this.network$ = this.stateService.networkChanged$;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { Subscription, Observable, of, combineLatest, throwError } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EtaService } from '../../services/eta.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { feeLevels } from '../../app.constants';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -12,6 +12,8 @@ import { Location } from '@angular/common';
|
||||
import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
import { colorFromRetarget } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mempool-blocks',
|
||||
@@ -93,6 +95,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
private router: Router,
|
||||
public stateService: StateService,
|
||||
public cacheService: CacheService,
|
||||
private etaService: EtaService,
|
||||
private themeService: ThemeService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@@ -387,6 +390,37 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i)));
|
||||
}
|
||||
|
||||
isDA(height: number): boolean {
|
||||
if (this.chainTip === -1) {
|
||||
return false;
|
||||
}
|
||||
const isDA = height % 2016 === 0 && this.stateService.network === '';
|
||||
if (isDA && !this.cacheService.daCache[height]) {
|
||||
this.cacheService.daCache[height] = { adjustment: 0 };
|
||||
this.difficultyAdjustments$.pipe(
|
||||
filter(da => !!da),
|
||||
switchMap(da => {
|
||||
const mempoolBlocksAvailable = this.chainTip && this.mempoolBlockStyles[height - this.chainTip - 1];
|
||||
return mempoolBlocksAvailable ? of(da) : throwError(() => new Error());
|
||||
}),
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(100),
|
||||
take(3)
|
||||
)
|
||||
),
|
||||
tap(da => {
|
||||
const adjustment = parseFloat((1 + da.difficultyChange / 100).toFixed(4));
|
||||
if (adjustment !== this.cacheService.daCache[height].adjustment) {
|
||||
this.cacheService.daCache[height].adjustment = adjustment;
|
||||
this.mempoolBlockStyles[height - this.chainTip - 1].background = colorFromRetarget(adjustment);
|
||||
}
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
return isDA;
|
||||
}
|
||||
|
||||
getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) {
|
||||
const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0);
|
||||
const usedBlockSpace = 100 - emptyBackgroundSpacePercentage;
|
||||
@@ -410,7 +444,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
return {
|
||||
'right': this.containerOffset + index * this.blockOffset + 'px',
|
||||
'background': backgroundGradients.join(',') + ')'
|
||||
'background': this.isDA(mempoolBlock.height) ? colorFromRetarget(this.cacheService.daCache[mempoolBlock.height]?.adjustment || 1) : backgroundGradients.join(',') + ')'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -901,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
||||
this.tx.flags = getTransactionFlags(this.tx);
|
||||
this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network);
|
||||
this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : [];
|
||||
this.checkAccelerationEligibility();
|
||||
} else {
|
||||
|
||||
@@ -3670,6 +3670,39 @@ export const restApiDocsData = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "mining",
|
||||
httpRequestMethod: "GET",
|
||||
fragment: "get-difficulty-adjustment-by-height",
|
||||
title: "GET Difficulty Adjustment",
|
||||
description: {
|
||||
default: "<p>Returns difficulty adjustment for the block at the specified <code>:blockHeight</code>. If no adjustment happened at that height, an empty response is returned.</p>"
|
||||
},
|
||||
urlString: "/v1/mining/difficulty-adjustment/:blockHeight",
|
||||
showConditions: [""],
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `/api/v1/mining/difficulty-adjustment/%{1}`,
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: [`756000`],
|
||||
response: `{
|
||||
"time": "2022-09-28T02:56:34.000Z",
|
||||
"height": 756000,
|
||||
"difficulty": 31360548173144.9,
|
||||
"adjustment": 0.97863
|
||||
}`
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "mining",
|
||||
|
||||
@@ -239,7 +239,7 @@ export interface TransactionStripped {
|
||||
acc?: boolean;
|
||||
flags?: number | null;
|
||||
time?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -315,6 +315,12 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getDifficultyAdjustmentByHeight$(height: number): Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustment/${height}`
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
|
||||
|
||||
@@ -23,6 +23,7 @@ export class CacheService {
|
||||
blockLoading: { [height: number]: boolean } = {};
|
||||
copiesInBlockQueue: { [height: number]: number } = {};
|
||||
blockPriorities: number[] = [];
|
||||
daCache: { [height: number]: { adjustment: number, exact?: boolean } } = {};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -128,6 +129,7 @@ export class CacheService {
|
||||
this.blockLoading = {};
|
||||
this.copiesInBlockQueue = {};
|
||||
this.blockPriorities = [];
|
||||
this.daCache = {};
|
||||
}
|
||||
|
||||
getCachedBlock(height) {
|
||||
|
||||
@@ -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 => ({
|
||||
@@ -239,3 +240,25 @@ export function md5(inputString): string {
|
||||
}
|
||||
return rh(a)+rh(b)+rh(c)+rh(d);
|
||||
}
|
||||
|
||||
export function colorFromRetarget(da: number): string {
|
||||
const minDA = 0.95;
|
||||
const maxDA = 1.05;
|
||||
const midDA = 1;
|
||||
|
||||
const red = { r: 220, g: 53, b: 69 };
|
||||
const grey = { r: 108, g: 117, b: 125 };
|
||||
const green = { r: 59, g: 204, b: 73 };
|
||||
|
||||
const interpolateColor = (color1, color2, ratio) => {
|
||||
ratio = Math.min(1, Math.max(0, ratio));
|
||||
const r = Math.round(color1.r + ratio * (color2.r - color1.r));
|
||||
const g = Math.round(color1.g + ratio * (color2.g - color1.g));
|
||||
const b = Math.round(color1.b + ratio * (color2.b - color1.b));
|
||||
return `rgba(${r}, ${g}, ${b}, 0.7)`;
|
||||
}
|
||||
|
||||
return da <= midDA ?
|
||||
interpolateColor(red, grey, (da - minDA) / (midDA - minDA)) :
|
||||
interpolateColor(grey, green, (da - midDA) / (maxDA - midDA));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
2
unfurler/package-lock.json
generated
2
unfurler/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Renderer for mempool open graph link preview images",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user