Compare commits

..

7 Commits

Author SHA1 Message Date
Mononaut
1037b50851 strip non-essential data from redis cache txs 2023-07-25 16:35:21 +09:00
Mononaut
a3c82e44d9 store redis mempool in sharded json object 2023-07-25 16:12:39 +09:00
Mononaut
78a0059d01 Add network and schema versioning to redis cache 2023-07-25 16:12:39 +09:00
Mononaut
172d4f9dfc Add transactions to Redis cache in manageable batches 2023-07-25 16:12:38 +09:00
Mononaut
97ecc7b90f Fix redis feature merge conflicts 2023-07-25 16:12:38 +09:00
Mononaut
381d2f0b1f Add RBF data to Redis cache 2023-07-25 16:12:38 +09:00
Mononaut
38a79249a3 Implement Redis cache for block and mempool data 2023-07-25 16:12:37 +09:00
192 changed files with 2258 additions and 6122 deletions

View File

@@ -1,7 +1,6 @@
version: 2
updates:
- package-ecosystem: npm
versioning-strategy: increase
directory: "/backend"
schedule:
interval: daily
@@ -15,21 +14,6 @@ updates:
- package-ecosystem: npm
directory: "/frontend"
versioning-strategy: increase
groups:
frontend-angular-dependencies:
patterns:
- "@angular*"
- "@ng-*"
- "ngx-*"
frontend-jest-dependencies:
patterns:
- "@types/jest"
- "jest"
frontend-eslint-dependencies:
patterns:
- "@typescript-eslint*"
- "eslint"
schedule:
interval: daily
open-pull-requests-limit: 10

View File

@@ -27,8 +27,8 @@ jobs:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install 1.63.x Rust toolchain
uses: dtolnay/rust-toolchain@1.63
- name: Install 1.70.x Rust toolchain
uses: dtolnay/rust-toolchain@1.70
- name: Install
if: ${{ matrix.flavor == 'dev'}}
@@ -47,7 +47,7 @@ jobs:
- name: Unit Tests
if: ${{ matrix.flavor == 'dev'}}
run: npm run test:ci
run: npm run test
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Build

View File

@@ -38,7 +38,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16.15.0
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json

View File

@@ -13,7 +13,7 @@ the terms of (at your option) either:
proxy statement published on <https://mempool.space/about>.
However, this copyright license does not include an implied right or license to
use our trademarks: The Mempool Open Source Project®, mempool.space™, the
use our trademarks: The Mempool Open Source Project, mempool.space™, the
mempool Logo™, the mempool.space Vertical Logo™, the mempool.space Horizontal
Logo™, the mempool Square Logo™, and the mempool Blocks logo™ are registered
trademarks or trademarks of Mempool Space K.K in Japan, the United States,

View File

@@ -1,4 +1,4 @@
# The Mempool Open Source Project® [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
# The Mempool Open Source Project [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4

3
backend/.gitignore vendored
View File

@@ -45,6 +45,3 @@ testem.log
#System Files
.DS_Store
Thumbs.db
# package folder (npm run package output)
/package

View File

@@ -8,7 +8,6 @@
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CACHE_ENABLED": true,
"CLEAR_PROTECTION_MINUTES": 20,
"RECOMMENDED_FEE_PERCENTILE": 50,
"BLOCK_WEIGHT_UNITS": 4000000,
@@ -32,8 +31,7 @@
"CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
"ALLOW_UNREACHABLE": true
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -50,8 +48,7 @@
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"RETRY_UNIX_SOCKET_AFTER": 30000,
"FALLBACK": []
"RETRY_UNIX_SOCKET_AFTER": 30000
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
@@ -117,6 +114,10 @@
"USERNAME": "",
"PASSWORD": ""
},
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
@@ -135,9 +136,5 @@
"trusted",
"servers"
]
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api",
"ACCELERATIONS": false
}
}

View File

@@ -1,17 +0,0 @@
#/bin/sh
set -e
# Remove previous dist folder
rm -rf dist
# Build new dist folder
npm run build
# Remove previous package folder
rm -rf package
# Move JS and deps
mv dist package
cp -R node_modules package
# Remove symlink for rust-gbt and insert real folder
rm package/node_modules/rust-gbt
cp -R rust-gbt package/node_modules
# Clean up deps
npm run package-rm-build-deps

View File

@@ -1,12 +0,0 @@
#/bin/sh
set -e
# Cleaning up inside the node_modules folder
cd package/node_modules
rm -r \
typescript \
@typescript-eslint \
@napi-rs \
./rust-gbt/src \
./rust-gbt/Cargo.toml \
./rust-gbt/build.rs

View File

@@ -17,9 +17,9 @@
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.6.0",
"redis": "^4.6.6",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.13.0"
@@ -6102,9 +6102,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz",
"integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
@@ -12212,9 +12212,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz",
"integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz",
"integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==",
"requires": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",

View File

@@ -22,20 +22,19 @@
"main": "index.ts",
"scripts": {
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
"build": "npm run rust-build && npm run tsc && npm run create-resources",
"build": "npm run build-rust && npm run tsc && npm run create-resources",
"create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
"package": "./npm_package.sh",
"package-rm-build-deps": "./npm_package_rm_build_deps.sh",
"package": "npm run build && rm -rf package && mv dist package && mv node_modules package && mv rust-gbt package && npm run package-rm-build-deps",
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint @napi-rs ../rust-gbt/target ../rust-gbt/node_modules ../rust-gbt/src)",
"start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=16384 dist/index.js",
"reindex-updated-pools": "npm run start-production --update-pools",
"reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks",
"test": "./node_modules/.bin/jest --coverage",
"test:ci": "CI=true ./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"",
"rust-build": "cd rust-gbt && npm run build-release"
"build-rust": "cd rust-gbt && npm install"
},
"dependencies": {
"@babel/core": "^7.21.3",
@@ -46,7 +45,7 @@
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.11",
"mysql2": "~3.6.0",
"mysql2": "~3.5.2",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",

View File

@@ -6,8 +6,6 @@ authors = ["mononaut"]
edition = "2021"
publish = false
[workspace]
[lib]
crate-type = ["cdylib"]

View File

@@ -12,10 +12,6 @@ export interface ThreadTransaction {
effectiveFeePerVsize: number
inputs: Array<number>
}
export interface ThreadAcceleration {
uid: number
delta: number
}
export class GbtGenerator {
constructor()
/**
@@ -23,13 +19,13 @@ export class GbtGenerator {
*
* Rejects if the thread panics or if the Mutex is poisoned.
*/
make(mempool: Array<ThreadTransaction>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
make(mempool: Array<ThreadTransaction>, maxUid: number): Promise<GbtResult>
/**
* # Errors
*
* Rejects if the thread panics or if the Mutex is poisoned.
*/
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, maxUid: number): Promise<GbtResult>
}
/**
* The result from calling the gbt function.

View File

@@ -1,6 +1,6 @@
use crate::{
u32_hasher_types::{u32hashset_new, U32HasherState},
ThreadTransaction, thread_acceleration::ThreadAcceleration,
ThreadTransaction,
};
use std::{
cmp::Ordering,
@@ -88,49 +88,44 @@ impl Ord for AuditTransaction {
}
#[inline]
fn calc_fee_rate(fee: u64, vsize: f64) -> f64 {
(fee as f64) / (if vsize == 0.0 { 1.0 } else { vsize })
fn calc_fee_rate(fee: f64, vsize: f64) -> f64 {
fee / (if vsize == 0.0 { 1.0 } else { vsize })
}
impl AuditTransaction {
pub fn from_thread_transaction(tx: &ThreadTransaction, maybe_acceleration: Option<Option<&ThreadAcceleration>>) -> Self {
let fee_delta = match maybe_acceleration {
Some(Some(acceleration)) => acceleration.delta,
_ => 0.0
};
let fee = (tx.fee as u64) + (fee_delta as u64);
pub fn from_thread_transaction(tx: &ThreadTransaction) -> Self {
// rounded up to the nearest integer
let is_adjusted = tx.weight < (tx.sigops * 20);
let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
let effective_fee_per_vsize = if is_adjusted || fee_delta > 0.0 {
calc_fee_rate(fee, f64::from(sigop_adjusted_weight) / 4.0)
let effective_fee_per_vsize = if is_adjusted {
calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0)
} else {
tx.effective_fee_per_vsize
};
Self {
uid: tx.uid,
order: tx.order,
fee,
fee: tx.fee as u64,
weight: tx.weight,
sigop_adjusted_weight,
sigop_adjusted_vsize,
sigops: tx.sigops,
adjusted_fee_per_vsize: calc_fee_rate(fee, f64::from(sigop_adjusted_vsize)),
adjusted_fee_per_vsize: calc_fee_rate(tx.fee, f64::from(sigop_adjusted_vsize)),
effective_fee_per_vsize,
dependency_rate: f64::INFINITY,
inputs: tx.inputs.clone(),
relatives_set_flag: false,
ancestors: u32hashset_new(),
children: u32hashset_new(),
ancestor_fee: fee,
ancestor_fee: tx.fee as u64,
ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
ancestor_sigops: tx.sigops,
score: 0.0,
used: false,
modified: false,
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize || fee_delta > 0.0,
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize,
}
}
@@ -161,7 +156,7 @@ impl AuditTransaction {
// grows, so if we think of 0 as "grew infinitely" then dependency_rate would be
// the smaller of the two. If either side is NaN, the other side is returned.
self.dependency_rate.min(calc_fee_rate(
self.ancestor_fee,
self.ancestor_fee as f64,
f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
))
}
@@ -177,7 +172,7 @@ impl AuditTransaction {
#[inline]
fn calc_new_score(&mut self) {
self.score = self.adjusted_fee_per_vsize.min(calc_fee_rate(
self.ancestor_fee,
self.ancestor_fee as f64,
f64::from(self.ancestor_sigop_adjusted_vsize),
));
}

View File

@@ -5,7 +5,7 @@ use tracing::{info, trace};
use crate::{
audit_transaction::{partial_cmp_uid_score, AuditTransaction},
u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration,
GbtResult, ThreadTransactionsMap,
};
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
@@ -53,13 +53,7 @@ impl Ord for TxPriority {
// TODO: Make gbt smaller to fix these lints.
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAcceleration], max_uid: usize) -> GbtResult {
let mut indexed_accelerations = Vec::with_capacity(max_uid + 1);
indexed_accelerations.resize(max_uid + 1, None);
for acceleration in accelerations {
indexed_accelerations[acceleration.uid as usize] = Some(acceleration);
}
pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
let mempool_len = mempool.len();
let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
audit_pool.resize(max_uid + 1, None);
@@ -69,8 +63,7 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat
info!("Initializing working structs");
for (uid, tx) in &mut *mempool {
let acceleration = indexed_accelerations.get(*uid as usize);
let audit_tx = AuditTransaction::from_thread_transaction(tx, acceleration.copied());
let audit_tx = AuditTransaction::from_thread_transaction(tx);
// Safety: audit_pool and mempool_stack must always contain the same transactions
audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
mempool_stack.push(*uid);
@@ -335,15 +328,13 @@ fn set_relatives(txid: u32, audit_pool: &mut AuditPool) {
let mut total_sigops: u32 = 0;
for ancestor_id in &ancestors {
if let Some(ancestor) = audit_pool
let Some(ancestor) = audit_pool
.get(*ancestor_id as usize)
.expect("audit_pool contains all ancestors")
{
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
} else { todo!() };
.expect("audit_pool contains all ancestors") else { todo!() };
total_fee += ancestor.fee;
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
total_sigops += ancestor.sigops;
}
if let Some(Some(tx)) = audit_pool.get_mut(txid as usize) {

View File

@@ -9,7 +9,6 @@
use napi::bindgen_prelude::Result;
use napi_derive::napi;
use thread_transaction::ThreadTransaction;
use thread_acceleration::ThreadAcceleration;
use tracing::{debug, info, trace};
use tracing_log::LogTracer;
use tracing_subscriber::{EnvFilter, FmtSubscriber};
@@ -20,7 +19,6 @@ use std::sync::{Arc, Mutex};
mod audit_transaction;
mod gbt;
mod thread_transaction;
mod thread_acceleration;
mod u32_hasher_types;
use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState};
@@ -76,11 +74,10 @@ impl GbtGenerator {
///
/// Rejects if the thread panics or if the Mutex is poisoned.
#[napi]
pub async fn make(&self, mempool: Vec<ThreadTransaction>, accelerations: Vec<ThreadAcceleration>, max_uid: u32) -> Result<GbtResult> {
pub async fn make(&self, mempool: Vec<ThreadTransaction>, max_uid: u32) -> Result<GbtResult> {
trace!("make: Current State {:#?}", self.thread_transactions);
run_task(
Arc::clone(&self.thread_transactions),
accelerations,
max_uid as usize,
move |map| {
for tx in mempool {
@@ -99,13 +96,11 @@ impl GbtGenerator {
&self,
new_txs: Vec<ThreadTransaction>,
remove_txs: Vec<u32>,
accelerations: Vec<ThreadAcceleration>,
max_uid: u32,
) -> Result<GbtResult> {
trace!("update: Current State {:#?}", self.thread_transactions);
run_task(
Arc::clone(&self.thread_transactions),
accelerations,
max_uid as usize,
move |map| {
for tx in new_txs {
@@ -146,7 +141,6 @@ pub struct GbtResult {
/// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
async fn run_task<F>(
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
accelerations: Vec<ThreadAcceleration>,
max_uid: usize,
callback: F,
) -> Result<GbtResult>
@@ -165,7 +159,7 @@ where
callback(&mut map);
info!("Starting gbt algorithm for {} elements...", map.len());
let result = gbt::gbt(&mut map, &accelerations, max_uid);
let result = gbt::gbt(&mut map, max_uid);
info!("Finished gbt algorithm for {} elements...", map.len());
debug!(

View File

@@ -1,8 +0,0 @@
use napi_derive::napi;
#[derive(Debug)]
#[napi(object)]
pub struct ThreadAcceleration {
pub uid: u32,
pub delta: f64, // fee delta
}

View File

@@ -23,8 +23,8 @@
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
"AUDIT": true,
"ADVANCED_GBT_AUDIT": true,
"ADVANCED_GBT_MEMPOOL": true,
@@ -33,8 +33,7 @@
"MAX_BLOCKS_BULK_QUERY": 999,
"DISK_CACHE_BLOCK_INTERVAL": 999,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
"ALLOW_UNREACHABLE": true
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -51,8 +50,7 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": 888,
"FALLBACK": []
"RETRY_UNIX_SOCKET_AFTER": 888
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -94,6 +92,10 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@@ -127,10 +129,6 @@
"AUDIT_START_HEIGHT": 774000,
"SERVERS": []
},
"MEMPOOL_SERVICES": {
"API": "",
"ACCELERATIONS": false
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"

View File

@@ -1,24 +0,0 @@
import { Common } from '../../api/common';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
const randomTransactions = require('./test-data/transactions-random.json');
const replacedTransactions = require('./test-data/transactions-replaced.json');
const rbfTransactions = require('./test-data/transactions-rbfs.json');
describe('Mempool Utils', () => {
test('should detect RBF transactions with fast method', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
test.only('should detect RBF transactions with scalable method', () => {
const newTransactions = rbfTransactions.concat(randomTransactions);
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
expect(Object.values(result).length).toEqual(2);
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
});
});

View File

@@ -1,8 +1,4 @@
import {
calcBitsDifference,
calcDifficultyAdjustment,
DifficultyAdjustment,
} from '../../api/difficulty-adjustment';
import { calcDifficultyAdjustment, DifficultyAdjustment } from '../../api/difficulty-adjustment';
describe('Mempool Difficulty Adjustment', () => {
test('should calculate Difficulty Adjustments properly', () => {
@@ -90,46 +86,4 @@ describe('Mempool Difficulty Adjustment', () => {
expect(result).toStrictEqual(vector[1]);
}
});
test('should calculate Difficulty change from bits fields of two blocks', () => {
// Check same exponent + check min max for output
expect(calcBitsDifference(0x1d000200, 0x1d000100)).toEqual(100);
expect(calcBitsDifference(0x1d000400, 0x1d000100)).toEqual(300);
expect(calcBitsDifference(0x1d000800, 0x1d000100)).toEqual(300); // Actually 700
expect(calcBitsDifference(0x1d000100, 0x1d000200)).toEqual(-50);
expect(calcBitsDifference(0x1d000100, 0x1d000400)).toEqual(-75);
expect(calcBitsDifference(0x1d000100, 0x1d000800)).toEqual(-75); // Actually -87.5
// Check new higher exponent
expect(calcBitsDifference(0x1c000200, 0x1d000001)).toEqual(100);
expect(calcBitsDifference(0x1c000400, 0x1d000001)).toEqual(300);
expect(calcBitsDifference(0x1c000800, 0x1d000001)).toEqual(300);
expect(calcBitsDifference(0x1c000100, 0x1d000002)).toEqual(-50);
expect(calcBitsDifference(0x1c000100, 0x1d000004)).toEqual(-75);
expect(calcBitsDifference(0x1c000100, 0x1d000008)).toEqual(-75);
// Check new lower exponent
expect(calcBitsDifference(0x1d000002, 0x1c000100)).toEqual(100);
expect(calcBitsDifference(0x1d000004, 0x1c000100)).toEqual(300);
expect(calcBitsDifference(0x1d000008, 0x1c000100)).toEqual(300);
expect(calcBitsDifference(0x1d000001, 0x1c000200)).toEqual(-50);
expect(calcBitsDifference(0x1d000001, 0x1c000400)).toEqual(-75);
expect(calcBitsDifference(0x1d000001, 0x1c000800)).toEqual(-75);
// Check error when exponents are too far apart
expect(() => calcBitsDifference(0x1d000001, 0x1a000800)).toThrow(
/Impossible exponent difference/
);
// Check invalid inputs
expect(() => calcBitsDifference(0x7f000001, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0, 0x1a000800)).toThrow(/Invalid bits/);
expect(() => calcBitsDifference(100.2783, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0x00800000, 0x1a000800)).toThrow(
/Invalid bits/
);
expect(() => calcBitsDifference(0x1c000000, 0x1a000800)).toThrow(
/Invalid bits/
);
});
});

View File

@@ -1,277 +0,0 @@
[
{
"txid": "13f007241d78e8b0b4e57d2ae3fd37bcfe3226534d7cadeba5a549860d960db0",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "cb8f206f4e88bec97107089f3e9e61d50cde53d4541992ae19759b71103cf75c",
"vout": 0,
"prevout": {
"scriptpubkey": "0014fd6d15ff832c12f1ff04a5ccd5039f7227b260bd",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 fd6d15ff832c12f1ff04a5ccd5039f7227b260bd",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1ql4k3tlur9sf0rlcy5hxd2qulwgnmyc9akehvth",
"value": 610677
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304302205c430b36ebd2bb327951d83440af1f58f127871b2baada4c4dde2bc0b6721f56021f3445099f1a40e35baeda32e8e3727b505ffba0d882b11f498c7762f4184e9901",
"0236b5edd4fbbcfb045960e42ec8a9968944084785932e32940e8cd2583b37da67"
],
"is_coinbase": false,
"sequence": 2147483648
}
],
"vout": [
{
"scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd",
"value": 344697
},
{
"scriptpubkey": "00147dee8a7a38abbfb00dbfba365c8d6712934cc491",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7dee8a7a38abbfb00dbfba365c8d6712934cc491",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q0hhg573c4wlmqrdlhgm9ert8z2f5e3y3lf9hvx",
"value": 265396
}
],
"size": 224,
"weight": 572,
"fee": 584,
"status": {
"confirmed": false
},
"order": 2953680397,
"vsize": 143,
"adjustedVsize": 143,
"sigops": 5,
"feePerVsize": 4.083916083916084,
"adjustedFeePerVsize": 4.083916083916084,
"effectiveFeePerVsize": 4.083916083916084,
"firstSeen": 1691222538,
"uid": 526973,
"inputs": [
526728
],
"position": {
"block": 7,
"vsize": 21429708.5
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "8e89b20f8a7fadb0e4cdbe57a00eee224f5076bac5387fc276916724e7c4a16a",
"version": 2,
"locktime": 800571,
"vin": [
{
"txid": "35e16762459539f3a8e52c5dee6a9ccaa9e9268efed33aa2c6e1b7805e849f24",
"vout": 0,
"prevout": {
"scriptpubkey": "0014d4f16ef275b3e1c4a4ecbef55a164933e0f6460f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d4f16ef275b3e1c4a4ecbef55a164933e0f6460f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q6nckaun4k0suff8vhm6459jfx0s0v3s0ff4ukl",
"value": 1528924
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022019008b26e885bb43da25a11ffac147a057722072eedb68411f114f6e7eb82ebc02201b618264bb97756b88fc3bbc365b73044ac18b33b1067e31cfd5bcd0f50ed2c701",
"039b71145070bd3e8af28e27fa577f2e12ab6bb4e212d3eeaef08b4bc39e8cbc13"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "67c27ed0f767526234bcd5f795a31fab8ec4d0251bf12c68f2746951f4110d90",
"vout": 3,
"prevout": {
"scriptpubkey": "0014a7c3d613b321375054b2ac9b6114367bc034ad6f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a7c3d613b321375054b2ac9b6114367bc034ad6f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5lpavyanyym4q49j4jdkz9pk00qrftt0yqzvk3",
"value": 436523
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204e67285fc656bc45ed082499b076d5dba2fa21d0d7e64a0ae52b19d69a11760002200f037d81ee540b74397844513b72b08ed92b06db76bd20b08f7a0a3b36ab13d501",
"02a3ebae85f0225b6fbb5ff060afce683a4683507a57544605a29ee7d287e591b4"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "21c38fb9a2521e438c614f53b19ddd7a5594bcc4b77480e762fd4b702fad3374",
"vout": 1,
"prevout": {
"scriptpubkey": "00149660e34ef88106536c816c037b5b28dd64a812e2",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 9660e34ef88106536c816c037b5b28dd64a812e2",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qjeswxnhcsyr9xmypdsphkkegm4j2syhztgzxv4",
"value": 758149
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022021b556f0aa99329076bcc435338aceaf534963efcab306931b1b2b0461e16e0c02203a78942a3745c4da656bddfd8cf16b85dc04d652904e88682127cdd9ca63339001",
"0298963be4a8f66aca9fcf1c6dc95547aeaa82347543190c91e094c2321142b9f0"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "aa998dbae65240a7386bf7d468459551d99c3de8e2f9057ff5f2d38e17daf788",
"vout": 0,
"prevout": {
"scriptpubkey": "00147bb7413a39943b21ded98ad5e6ad7a222d273e17",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7bb7413a39943b21ded98ad5e6ad7a222d273e17",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q0wm5zw3ejsajrhke3t27dtt6ygkjw0sh9lltg6",
"value": 1067200
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402205e2269f7d4ee0513b34354c38e920aef2dabac6f4350afb2dd105ff3ee43ae7b02202870322f2cb85cb0b2b0e38152f018bfff271dc3ec5aed0515854d0b259aaf3d01",
"03b87320cf3263a644a0d3f89c1b4a7304d9dfda9eb8c891560716abcb73e88b99"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "230253d195d779d4688ba16993985cd27b2e7a687d8b889b3bc63f19ece36f20",
"vout": 0,
"prevout": {
"scriptpubkey": "001439647bd997819d12dfc72b0fb9ff9ffcb84946f8",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 39647bd997819d12dfc72b0fb9ff9ffcb84946f8",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q89j8hkvhsxw39h789v8mnlulljuyj3hc9zve97",
"value": 361950
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204f7ca868bb9b92a07fecdc6b9dd56e4e1d007ca1405952f98ed6bc416345b5f2022055320a97791417abf6628fcf6513ac5785b06c630f854d8595e96ea06c3841d301",
"03a3ffe8e3ef2eea129b227e9658164bae0a6d21c17da6de9973ba34d9e04b21a0"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "670771e265a0b62dbd3c1fec2b865177eaf0bafd0ae49dd40a1c9fcd9a847a81",
"vout": 0,
"prevout": {
"scriptpubkey": "0014d45d1b0022c7387e42c5452ced561bdb8fd4b521",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d45d1b0022c7387e42c5452ced561bdb8fd4b521",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q63w3kqpzcuu8usk9g5kw64smmw8afdfpxmc2m0",
"value": 453275
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022071312921800441903b2099e723add8702dd0f92ec11526ff87acf6967ec64cbd02203deabe7ed56d5daaa9a95c5a607b1ab705ff1c46bc6984a6dca120e63a91768601",
"0257302ac8d9c4c8f9b1744f19bb432359326b9cc7bdddeeab9202749a6d92be58"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "0af82159eee2b69242f2ff032636e410b67ec1ace52e55fb0d20ed814cd64803",
"vout": 0,
"prevout": {
"scriptpubkey": "001459e4d6bfefc6b45f955a69c4aeca26348e9d54ed",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 59e4d6bfefc6b45f955a69c4aeca26348e9d54ed",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qt8jdd0l0c669l926d8z2aj3xxj8f648dtyn7tc",
"value": 439961
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3044022027540322e92c23c5513aa2587e7feb56a8ce82f879269d6b3cbd425634b44f8e022045572dee7262b02130bfe32d8aa8abbfaa64e101abfc819bba5380c78876692d01",
"03fe02262d87f4a5289d3dd66e3d9a74cd49fa1cad0249284a7451896a827249a5"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "68cf9c784870a4f888f044755f7ce318557f652461db8ef887d279672f186018",
"vout": 0,
"prevout": {
"scriptpubkey": "001454822b2d5d52597a78b630921cf439a41e32f2f9",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 54822b2d5d52597a78b630921cf439a41e32f2f9",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q2jpzkt2a2fvh579kxzfpeape5s0r9uhewhl5n4",
"value": 227639
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402203ad511d6a8730748b8828bc38897d360451adf620ebdc1d229c08c097c80bef202202f50c793d95b5200cf2258e03896a3be7720df0eb3b8c810c86db74341a7e83e01",
"0294992e9f4546e6e119741f908411ae531e9d1ff732d69b4dff8172aaf2a4b216"
],
"is_coinbase": false,
"sequence": 4294967293
},
{
"txid": "793f01dfdb19bf41f958fd917c16d9c4dd5d5e1a5c0434bfdb367212659d1b5b",
"vout": 0,
"prevout": {
"scriptpubkey": "0014f54edf8ae647b5300e2674523254e923d93d169f",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 f54edf8ae647b5300e2674523254e923d93d169f",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q748dlzhxg76nqr3xw3fry48fy0vn695lvhlkxv",
"value": 227070
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402206e807ab616f4f2887ba703ae744d856142d9aca8128698419bbb67fb4fad8177022060fc65c7cd66baa88ad1e1d317a6edd5f6cb52fe8bff6e5405ffa1acf9d945d901",
"02a0ad0167c6e9edf62677404d74d3b80ea276e47e758ffaa6ca17bd65ac79f7aa"
],
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "00148a5c45ccfc29d209940d94525e2edb7743a1ad8a",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 8a5c45ccfc29d209940d94525e2edb7743a1ad8a",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q3fwytn8u98fqn9qdj3f9utkmwap6rtv2ym33zm",
"value": 5500000
}
],
"size": 1375,
"weight": 2605,
"fee": 691,
"status": {
"confirmed": false
},
"order": 1788986599,
"vsize": 651,
"adjustedVsize": 651.25,
"sigops": 9,
"feePerVsize": 1.0610364683301343,
"adjustedFeePerVsize": 1.0610364683301343,
"effectiveFeePerVsize": 1.0610364683301343,
"firstSeen": 1691163298,
"uid": 120494,
"inputs": [],
"position": {
"block": 7,
"vsize": 93780091.5
},
"bestDescendant": null,
"cpfpChecked": true
}
]

View File

@@ -1,121 +0,0 @@
[
{
"txid": "7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799995000
},
"scriptsig": "483045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "6a4c5058325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb",
"value": 155000
},
{
"scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2",
"value": 155000
},
{
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799675549
}
],
"size": 350,
"weight": 1400,
"fee": 9451,
"status": {
"confirmed": false
},
"order": 2798688215,
"vsize": 350,
"adjustedVsize": 350,
"sigops": 8,
"feePerVsize": 27.002857142857142,
"adjustedFeePerVsize": 27.002857142857142,
"effectiveFeePerVsize": 27.002857142857142,
"firstSeen": 1691218536,
"uid": 513598,
"inputs": [],
"position": {
"block": 0,
"vsize": 22166
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130",
"vout": 0,
"prevout": {
"scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz",
"value": 612917
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3045022100a0c23953ace5d022b7a6d45d1ae1730bf20a4d594bb5d4fa7aa80e4881b44d320220008f9b144805bb91995fc0f452a56e09f4ad16fa149d71ae9b5d57c742e8e2cc01",
"03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6"
],
"is_coinbase": false,
"sequence": 2147483649
}
],
"vout": [
{
"scriptpubkey": "0014199a98f9589364ffe5ef5bbae45ce5dfcbb873bd",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 199a98f9589364ffe5ef5bbae45ce5dfcbb873bd",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qrxdf372cjdj0le00twawgh89ml9msuaau62gk4",
"value": 611909
}
],
"size": 192,
"weight": 438,
"fee": 1008,
"status": {
"confirmed": false
},
"bestDescendant": null,
"descendants": null,
"adjustedFeePerVsize": 10.2283,
"sigops": 1,
"adjustedVsize": 109.5
}
]

View File

@@ -1,139 +0,0 @@
[
{
"txid": "008592364e21c1e3d62ba9538ac78a81779897b52100af5707ab063df98964f2",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799995000
},
"scriptsig": "483045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "6a4c5058325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b",
"scriptpubkey_type": "op_return",
"value": 0
},
{
"scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87",
"scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL",
"scriptpubkey_type": "p2sh",
"scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb",
"value": 155000
},
{
"scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2",
"value": 155000
},
{
"scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ",
"value": 799676250
}
],
"size": 350,
"weight": 1400,
"fee": 8750,
"status": {
"confirmed": false
},
"order": 4066675193,
"vsize": 350,
"adjustedVsize": 350,
"sigops": 8,
"feePerVsize": 25,
"adjustedFeePerVsize": 25,
"effectiveFeePerVsize": 25,
"firstSeen": 1691218516,
"uid": 512584,
"inputs": [],
"position": {
"block": 0,
"vsize": 13846
},
"bestDescendant": null,
"cpfpChecked": true
},
{
"txid": "b7981a624e4261c11f1246314d41e74be56af82eb557bcd054a5e0f94c023668",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130",
"vout": 0,
"prevout": {
"scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz",
"value": 612917
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"304402204dd10f14afa41bc76d8278140ff1ec3d3f87f2c207bbb5418cc76dab30d7f6a402207877cc9c6a2c724b6ea7a1c24ac00022469f194fd1a4bd8030bbca1787d3f5f301",
"03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6"
],
"is_coinbase": false,
"sequence": 2147483648
}
],
"vout": [
{
"scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd",
"value": 344697
},
{
"scriptpubkey": "00144c2671336ca8761863b4c68d64d4672491fec1b9",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4c2671336ca8761863b4c68d64d4672491fec1b9",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qfsn8zvmv4pmpsca5c6xkf4r8yjglasdesrawcx",
"value": 267636
}
],
"size": 225,
"weight": 573,
"fee": 584,
"status": {
"confirmed": false
},
"order": 1748369996,
"vsize": 143,
"adjustedVsize": 143.25,
"sigops": 5,
"feePerVsize": 4.076788830715532,
"adjustedFeePerVsize": 4.076788830715532,
"effectiveFeePerVsize": 4.076788830715532,
"firstSeen": 1691222376,
"uid": 526515,
"inputs": [],
"position": {
"block": 7,
"vsize": 22021095.5
},
"bestDescendant": null,
"cpfpChecked": true
}
]

View File

@@ -47,17 +47,11 @@ describe('Mempool Backend Config', () => {
DISK_CACHE_BLOCK_INTERVAL: 6,
MAX_PUSH_TX_SIZE_WEIGHT: 400000,
ALLOW_UNREACHABLE: true,
PRICE_UPDATES_PER_HOUR: 1,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null,
RETRY_UNIX_SOCKET_AFTER: 30000,
FALLBACK: [],
});
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
expect(config.CORE_RPC).toStrictEqual({
HOST: '127.0.0.1',
@@ -107,6 +101,11 @@ describe('Mempool Backend Config', () => {
PASSWORD: ''
});
expect(config.PRICE_DATA_SERVER).toStrictEqual({
TOR_URL: 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
CLEARNET_URL: 'https://price.bisq.wiz.biz/getAllMarketPrices'
});
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual({
MEMPOOL_API: 'https://mempool.space/api/v1',
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@@ -130,11 +129,6 @@ describe('Mempool Backend Config', () => {
SERVERS: []
});
expect(config.MEMPOOL_SERVICES).toStrictEqual({
API: "",
ACCELERATIONS: false,
});
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
@@ -169,9 +163,9 @@ describe('Mempool Backend Config', () => {
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.REDIS).toStrictEqual(fixture.REDIS);
});
@@ -186,50 +180,41 @@ describe('Mempool Backend Config', () => {
for (const [key, value] of Object.entries(jsonObj)) {
// We have a few cases where we can't follow the pattern
if (root === 'MEMPOOL' && key === 'HTTP_PORT') {
if (process.env.CI) {
console.log('skipping check for MEMPOOL_HTTP_PORT');
}
continue;
console.log('skipping check for MEMPOOL_HTTP_PORT');
return;
}
if (root) {
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
return;
} else {
parseJson(value, key);
}
break;
}
default: {
//The flattened string, i.e, __MEMPOOL_ENABLED__
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
//The string used as the environment variable, i.e, MEMPOOL_ENABLED
const envVarStr = `${root ? root : ''}_${key}`;
let defaultEntry;
//The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
if (Array.isArray(value)) {
defaultEntry = `${replaceStr}=\${${envVarStr}:=[]}`;
if (process.env.CI) {
console.log(`looking for ${defaultEntry} in the start.sh script`);
}
//Regex matching does not work with the array values
expect(startSh).toContain(defaultEntry);
} else {
defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
if (process.env.CI) {
console.log(`looking for ${defaultEntry} in the start.sh script`);
}
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
}
const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
console.log(`looking for ${defaultEntry} in the start.sh script`);
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
//The string that actually replaces the values in the config file
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
if (process.env.CI) {
console.log(`looking for ${sedStr} in the start.sh script`);
}
console.log(`looking for ${sedStr} in the start.sh script`);
expect(startSh).toContain(sedStr);
break;
}
else {
parseJson(value, key);
}
}
}
parseJson(fixture);
});
});

View File

@@ -1,5 +1,5 @@
import fs from 'fs';
import { GbtGenerator, ThreadTransaction } from 'rust-gbt';
import { GbtGenerator, ThreadTransaction } from '../../../rust-gbt';
import path from 'path';
const baseline = require('./test-data/target-template.json');
@@ -15,7 +15,7 @@ describe('Rust GBT', () => {
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
const rustGbt = new GbtGenerator();
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
const result = await rustGbt.make(mempool, [], maxUid);
const result = await rustGbt.make(mempool, maxUid);
const blocks: [string, number][][] = result.blocks.map(block => {
return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]);

View File

@@ -6,17 +6,16 @@ import rbfCache from './rbf-cache';
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
}
const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement
const isCensored = {}; // missing, without excuse
const isDisplaced = {};
let displacedWeight = 0;
@@ -29,9 +28,6 @@ class Audit {
const now = Math.round((Date.now() / 1000));
for (const tx of transactions) {
inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
}
}
// coinbase is always expected
if (transactions[0]) {
@@ -40,9 +36,8 @@ class Audit {
// look for transactions that were expected in the template, but missing from the mined block
for (const txid of projectedBlocks[0].transactionIds) {
if (!inBlock[txid]) {
// allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block
if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) {
rbf.push(txid);
if (rbfCache.isFullRbf(txid)) {
fullrbf.push(txid);
} else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) {
// tx is recent, may have reached the miner too late for inclusion
fresh.push(txid);
@@ -103,8 +98,8 @@ class Audit {
if (inTemplate[tx.txid]) {
matches.push(tx.txid);
} else {
if (rbfCache.has(tx.txid)) {
rbf.push(tx.txid);
if (rbfCache.isFullRbf(tx.txid)) {
fullrbf.push(tx.txid);
} else if (!isDisplaced[tx.txid]) {
added.push(tx.txid);
}
@@ -152,8 +147,7 @@ class Audit {
added,
fresh,
sigop: [],
fullrbf: rbf,
accelerated,
fullrbf,
score,
similarity,
};

View File

@@ -3,8 +3,7 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string);
$getMempoolTransactions(lastTxid: string);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
@@ -23,8 +22,6 @@ export interface AbstractBitcoinApi {
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
startHealthChecks(): void;
}
export interface BitcoinRpcCredentials {
host: string;

View File

@@ -60,13 +60,8 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
}
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
return Promise.resolve([]);
}
async $getTransactionHex(txId: string): Promise<string> {
@@ -355,7 +350,6 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
public startHealthChecks(): void {};
}
export default BitcoinApi;

View File

@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
import mempool from '../mempool';
import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks';
import bitcoinApi from './bitcoin-api-factory';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory';
import { Common } from '../common';
import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils';
@@ -214,7 +214,6 @@ class BitcoinRoutes {
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
});
return;
}
@@ -484,7 +483,7 @@ class BitcoinRoutes {
returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash;
} else {
const block = await bitcoinApi.$getBlock(nextHash);
const block = await bitcoinCoreApi.$getBlock(nextHash);
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
@@ -577,7 +576,7 @@ class BitcoinRoutes {
}
try {
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
const addressData = await bitcoinApi.$getScriptHash(req.params.address);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
@@ -598,7 +597,7 @@ class BitcoinRoutes {
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid;
}
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.address, lastTxId);
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {

View File

@@ -1,260 +1,112 @@
import config from '../../config';
import axios, { AxiosResponse } from 'axios';
import axios, { AxiosRequestConfig } from 'axios';
import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
interface FailoverHost {
host: string,
rtts: number[],
rtt: number
failures: number,
socket?: boolean,
outOfSync?: boolean,
unreachable?: boolean,
preferred?: boolean,
}
const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true, })
});
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
hosts: FailoverHost[];
multihost: boolean;
pollInterval: number = 60000;
pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create();
requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
class ElectrsApi implements AbstractBitcoinApi {
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
timeout: 10000,
} : {
timeout: 10000,
};
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
timeout: 10000,
};
unixSocketRetryTimeout;
activeAxiosConfig;
constructor() {
// setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return {
host: domain,
rtts: [],
rtt: Infinity,
failures: 0,
};
});
this.activeHost = {
host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL,
rtts: [],
rtt: 0,
failures: 0,
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
};
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
this.multihost = this.hosts.length > 1;
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
}
public startHealthChecks(): void {
// use axios interceptors to measure request rtt
this.pollConnection.interceptors.request.use((config) => {
config['meta'] = { startTime: Date.now() };
return config;
});
this.pollConnection.interceptors.response.use((response) => {
response.config['meta'].rtt = Date.now() - response.config['meta'].startTime;
return response;
});
if (this.multihost) {
this.pollHosts();
fallbackToTcpSocket() {
if (!this.unixSocketRetryTimeout) {
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
// Retry the unix socket after a few seconds
this.unixSocketRetryTimeout = setTimeout(() => {
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
this.unixSocketRetryTimeout = undefined;
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
}
// Use the TCP socket (reach a different esplora instance through nginx)
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
}
// start polling hosts to measure availability & rtt
private async pollHosts(): Promise<void> {
if (this.pollTimer) {
clearTimeout(this.pollTimer);
}
const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
} else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
}
}));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
// update rtts & sync status
for (let i = 0; i < results.length; i++) {
const host = this.hosts[i];
const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult<AxiosResponse<number, any>>).value : null;
if (result) {
const height = result.data;
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true;
} else {
host.outOfSync = false;
}
host.unreachable = false;
} else {
host.unreachable = true;
}
}
this.sortHosts();
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) {
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
} else if (this.activeHost.outOfSync) {
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
} else {
logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
}
this.electHost();
}
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
}
// sort hosts by connection quality, and update default fallback
private sortHosts(): void {
// sort by connection quality
this.hosts.sort((a, b) => {
if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) {
if (a.preferred === b.preferred) {
// lower rtt is best
return a.rtt - b.rtt;
} else { // unless we have a preferred host
return a.preferred ? -1 : 1;
}
} else { // or the host is out of sync
return (a.unreachable || a.outOfSync) ? 1 : -1;
}
});
if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) {
this.fallbackHost = this.hosts[1];
} else {
this.fallbackHost = this.hosts[0];
}
}
// depose the active host and choose the next best replacement
private electHost(): void {
this.activeHost.outOfSync = true;
this.activeHost.failures = 0;
this.sortHosts();
this.activeHost = this.hosts[0];
logger.warn(`Switching esplora host to ${this.activeHost.host}`);
}
private addFailure(host: FailoverHost): FailoverHost {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
this.electHost();
return this.activeHost;
} else {
return this.fallbackHost;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig;
let url;
if (host.socket) {
axiosConfig = { socketPath: host.host, timeout: 10000, responseType };
url = path;
} else {
axiosConfig = { timeout: 10000, responseType };
url = host.host + path;
}
return (method === 'post'
? this.requestConnection.post<T>(url, data, axiosConfig)
: this.requestConnection.get<T>(url, axiosConfig)
).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; })
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
.then((response) => response.data)
.catch((e) => {
let fallbackHost = this.fallbackHost;
if (e?.response?.status !== 404) {
logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
fallbackHost = this.addFailure(host);
}
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
// Retry immediately
return this.$query(method, path, data, responseType, fallbackHost, false);
return axiosConnection.get<T>(url, this.activeAxiosConfig)
.then((response) => response.data)
.catch((e) => {
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
throw e;
});
} else {
throw e;
}
});
}
public async $get<T>(path, responseType = 'json'): Promise<T> {
return this.$query<T>('get', path, null, responseType);
}
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
return this.$query<T>('post', path, data, responseType);
}
}
class ElectrsApi implements AbstractBitcoinApi {
private failoverRouter = new FailoverRouter();
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids');
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
}
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId);
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
}
async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
}
$getTransactionHex(txId: string): Promise<string> {
return this.failoverRouter.$get<string>('/tx/' + txId + '/hex');
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
}
$getBlockHeightTip(): Promise<number> {
return this.failoverRouter.$get<number>('/blocks/tip/height');
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
}
$getBlockHashTip(): Promise<string> {
return this.failoverRouter.$get<string>('/blocks/tip/hash');
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return this.failoverRouter.$get<string[]>('/block/' + hash + '/txids');
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
}
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs');
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs');
}
$getBlockHash(height: number): Promise<string> {
return this.failoverRouter.$get<string>('/block-height/' + height);
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
}
$getBlockHeader(hash: string): Promise<string> {
return this.failoverRouter.$get<string>('/block/' + hash + '/header');
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
}
$getBlock(hash: string): Promise<IEsploraApi.Block> {
return this.failoverRouter.$get<IEsploraApi.Block>('/block/' + hash);
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
}
$getRawBlock(hash: string): Promise<Buffer> {
return this.failoverRouter.$get<any>('/block/' + hash + '/raw', 'arraybuffer')
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
.then((response) => { return Buffer.from(response.data); });
}
@@ -283,11 +135,11 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
}
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends');
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
@@ -298,10 +150,6 @@ class ElectrsApi implements AbstractBitcoinApi {
}
return outspends;
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
}
export default ElectrsApi;

View File

@@ -28,17 +28,12 @@ import chainTips from './chain-tips';
import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
import os from 'os';
import { Worker } from 'worker_threads';
import path from 'path';
class Blocks {
private blocks: BlockExtended[] = [];
private blockSummaries: BlockSummary[] = [];
private currentBlockHeight = 0;
private currentBits = 0;
private currentDifficulty = 0;
private lastDifficultyAdjustmentTime = 0;
private previousDifficultyRetarget = 0;
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
@@ -112,16 +107,11 @@ class Blocks {
}
}
// Skip expensive lookups while mempool has priority
if (onlyCoinbase) {
try {
const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
if (coinbase && coinbase.vin[0].is_coinbase) {
return [coinbase];
} else {
const msg = `Expected a coinbase tx, but the backend API returned something else`;
logger.err(msg);
throw new Error(msg);
}
const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
return [coinbase];
} catch (e) {
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
@@ -146,17 +136,17 @@ class Blocks {
// Fetch remaining txs individually
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
transactionMap[txid] = tx;
totalFound++;
} catch (e) {
const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
if (!transactionMap[txid]) {
if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData);
transactionMap[txid] = tx;
totalFound++;
} catch (e) {
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
@@ -164,24 +154,8 @@ class Blocks {
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
}
// Require the first transaction to be a coinbase
const coinbase = transactionMap[txIds[0]];
if (!coinbase || !coinbase.vin[0].is_coinbase) {
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
logger.err(msg);
throw new Error(msg);
}
// Require all transactions to be present
// (we should have thrown an error already if a tx request failed)
if (txIds.some(txid => !transactionMap[txid])) {
const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
logger.err(msg);
throw new Error(msg);
}
// Return list of transactions, preserving block order
return txIds.map(txid => transactionMap[txid]);
return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
}
/**
@@ -410,8 +384,6 @@ class Blocks {
return;
}
const workerPool: Worker[] = [];
try {
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
@@ -426,67 +398,32 @@ class Blocks {
let newlyIndexed = 0;
let totalIndexed = indexedBlockSummariesHashesArray.length;
let indexedThisRun = 0;
let timer = Date.now();
const startedAt = Date.now();
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
const blocksToIndex = indexedBlocks.filter(block => !indexedBlockSummariesHashes[block.hash]);
if (!blocksToIndex.length) {
return;
}
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-summary-worker.js')));
}
const promises: Promise<void>[] = [];
// This function assigns a task to a worker
const assignTask = (worker: Worker): boolean => {
if (blocksToIndex.length === 0) {
return false;
} else {
worker.postMessage(blocksToIndex.shift());
return true;
for (const block of indexedBlocks) {
if (indexedBlockSummariesHashes[block.hash] === true) {
continue;
}
};
const handleResult = (height: number): void => {
// Logging
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
}
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
// Logging
indexedThisRun++;
totalIndexed++;
newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now();
indexedThisRun = 0;
}
};
// Start a task on each worker
for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
}
await Promise.all(promises);
if (newlyIndexed > 0) {
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else {
@@ -495,14 +432,6 @@ class Blocks {
} catch (e) {
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
}
}
@@ -527,18 +456,18 @@ class Blocks {
// Logging
let count = 0;
let countThisRun = 0;
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
let timer = new Date().getTime() / 1000;
const startedAt = new Date().getTime() / 1000;
for (const height of unindexedBlockHeights) {
// Logging
const hash = await bitcoinApi.$getBlockHash(height);
const elapsedSeconds = (Date.now() / 1000) - timer;
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = countThisRun / elapsedSeconds;
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = (countThisRun / elapsedSeconds);
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`);
timer = Date.now() / 1000;
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000;
countThisRun = 0;
}
@@ -599,7 +528,6 @@ class Blocks {
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
public async $generateBlockDatabase(): Promise<boolean> {
const workerPool: Worker[] = [];
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks;
@@ -618,18 +546,12 @@ class Blocks {
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0;
let newlyIndexed = 0;
const startedAt = Date.now();
let timer = Date.now();
if (currentBlockHeight >= lastBlockToIndex) {
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-worker.js')));
}
}
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
while (currentBlockHeight >= lastBlockToIndex) {
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights(
currentBlockHeight, endBlock);
if (missingBlockHeights.length <= 0) {
@@ -639,65 +561,33 @@ class Blocks {
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining);
const promises: Promise<void>[] = [];
// This function assigns a task to a worker
const assignTask = (worker: Worker): boolean => {
if (missingBlockHeights.length === 0) {
return false;
} else {
worker.postMessage({ height: missingBlockHeights.shift() });
return true;
for (const blockHeight of missingBlockHeights) {
if (blockHeight < lastBlockToIndex) {
break;
}
};
const handleResult = (height: number): void => {
indexedThisRun++;
totalIndexed++;
newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000 || height === lastBlockToIndex) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
++indexedThisRun;
++totalIndexed;
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
logger.debug(`Indexing block #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now();
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
}
};
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
// Start a task on each worker
for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
newlyIndexed++;
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
await Promise.all(promises);
currentBlockHeight -= chunkSize;
}
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
if (newlyIndexed > 0) {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else {
@@ -708,14 +598,6 @@ class Blocks {
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
loadingIndicators.setProgress('block-indexing', 100);
throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
}
return await BlocksRepository.$validateChain();
@@ -753,21 +635,17 @@ class Blocks {
const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentBits = block.bits;
this.currentDifficulty = block.difficulty;
if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash);
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
this.previousDifficultyRetarget = NaN;
} else {
this.previousDifficultyRetarget = calcBitsDifference(previousPeriodBlock.bits, block.bits);
}
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`);
}
} else {
@@ -791,14 +669,14 @@ class Blocks {
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
// fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) {
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
transactions[i].fee = (verboseBlock.tx[i].fee * 100_000_000) || 0;
if (config.MEMPOOL.BACKEND !== 'esplora') {
// fill in missing transaction fee data from verboseBlock
for (let i = 0; i < transactions.length; i++) {
if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
}
}
}
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
@@ -876,33 +754,18 @@ class Blocks {
if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
let adjustment;
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
adjustment = NaN;
} else {
adjustment = Math.round(
// calcBitsDifference returns +- percentage, +100 returns to positive, /100 returns to ratio.
// Instead of actually doing /100, just reduce the multiplier.
(calcBitsDifference(this.currentBits, block.bits) + 100) * 10000
) / 1000000; // Remove float point noise
}
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.timestamp,
height: block.height,
difficulty: block.difficulty,
adjustment,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
});
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
}
if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
this.previousDifficultyRetarget = NaN;
} else {
this.previousDifficultyRetarget = calcBitsDifference(this.currentBits, block.bits);
}
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentBits = block.bits;
this.currentDifficulty = block.difficulty;
}
// wait for pending async callbacks to finish
@@ -978,7 +841,7 @@ class Blocks {
}
const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -990,7 +853,7 @@ class Blocks {
}
public async $indexStaleBlock(hash: string): Promise<BlockExtended> {
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -1015,7 +878,7 @@ class Blocks {
}
// Bitcoin network, add our custom data on top
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(hash);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash);
if (block.stale) {
return await this.$indexStaleBlock(hash);
} else {
@@ -1050,7 +913,7 @@ class Blocks {
transactions: cpfpSummary.transactions.map(tx => {
return {
txid: tx.txid,
fee: tx.fee || 0,
fee: tx.fee,
vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize
@@ -1058,15 +921,10 @@ class Blocks {
}),
};
} else {
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(hash, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
summary = this.summarizeBlock(block);
height = block.height;
}
// Call Core RPC
const block = await bitcoinClient.getBlock(hash, 2);
summary = this.summarizeBlock(block);
height = block.height;
}
if (height == null) {
const block = await bitcoinApi.$getBlock(hash);
@@ -1189,17 +1047,8 @@ class Blocks {
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
if (cleanBlock.fee_amt_percentiles === null) {
let summary;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
summary = this.summarizeBlock(block);
}
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions);
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
}
@@ -1259,29 +1108,19 @@ class Blocks {
return this.currentBlockHeight;
}
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> {
let transactions = txs;
if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') {
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
}
if (!transactions) {
const block = await bitcoinClient.getBlock(hash, 2);
transactions = block.tx.map(tx => {
tx.fee *= 100_000_000;
return tx;
});
}
}
public async $indexCPFP(hash: string, height: number): Promise<void> {
const block = await bitcoinClient.getBlock(hash, 2);
const transactions = block.tx.map(tx => {
tx.fee *= 100_000_000;
return tx;
});
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
const summary = Common.calculateCpfp(height, transactions);
await this.$saveCpfp(hash, height, summary);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
return summary;
}
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {

View File

@@ -59,12 +59,10 @@ export class Common {
return arr;
}
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } {
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
// For small N, a naive nested loop is extremely fast, but it doesn't scale
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
added.forEach((addedTx) => {
added
.forEach((addedTx) => {
const foundMatches = deleted.filter((deletedTx) => {
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
return addedTx.fee > deletedTx.fee
@@ -75,40 +73,9 @@ export class Common {
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
});
if (foundMatches?.length) {
matches[addedTx.txid] = [...new Set(foundMatches)];
matches[addedTx.txid] = foundMatches;
}
});
} else {
// for large N, build a lookup table of prevouts we can check in ~constant time
const deletedSpendMap: { [txid: string]: { [vout: number]: MempoolTransactionExtended } } = {};
for (const tx of deleted) {
for (const vin of tx.vin) {
if (!deletedSpendMap[vin.txid]) {
deletedSpendMap[vin.txid] = {};
}
deletedSpendMap[vin.txid][vin.vout] = tx;
}
}
for (const addedTx of added) {
const foundMatches = new Set<MempoolTransactionExtended>();
for (const vin of addedTx.vin) {
const deletedTx = deletedSpendMap[vin.txid]?.[vin.vout];
if (deletedTx && deletedTx.txid !== addedTx.txid
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
&& addedTx.fee > deletedTx.fee
// The new transaction must pay more fee per kB than the replaced tx.
&& addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize
) {
foundMatches.add(deletedTx);
}
if (foundMatches.size) {
matches[addedTx.txid] = [...foundMatches];
}
}
}
}
return matches;
}
@@ -141,10 +108,9 @@ export class Common {
static stripTransaction(tx: TransactionExtended): TransactionStripped {
return {
txid: tx.txid,
fee: tx.fee || 0,
fee: tx.fee,
vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize,
};
}
@@ -494,7 +460,7 @@ export class Common {
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
let weightCount = 0;

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 65;
private static currentVersion = 64;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -548,11 +548,6 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
await this.updateToSchemaVersion(64);
}
if (databaseSchemaVersion < 65 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
await this.updateToSchemaVersion(65);
}
}
/**

View File

@@ -16,68 +16,6 @@ export interface DifficultyAdjustment {
expectedBlocks: number; // Block count
}
/**
* Calculate the difficulty increase/decrease by using the `bits` integer contained in two
* block headers.
*
* Warning: Only compare `bits` from blocks in two adjacent difficulty periods. This code
* assumes the maximum difference is x4 or /4 (as per the protocol) and will throw an
* error if an exponent difference of 2 or more is seen.
*
* @param {number} oldBits The 32 bit `bits` integer from a block header.
* @param {number} newBits The 32 bit `bits` integer from a block header in the next difficulty period.
* @returns {number} A floating point decimal of the difficulty change from old to new.
* (ie. 21.3 means 21.3% increase in difficulty, -21.3 is a 21.3% decrease in difficulty)
*/
export function calcBitsDifference(oldBits: number, newBits: number): number {
// Must be
// - integer
// - highest exponent is 0x20, so max value (as integer) is 0x207fffff
// - min value is 1 (exponent = 0)
// - highest bit of the number-part is +- sign, it must not be 1
const verifyBits = (bits: number): void => {
if (
Math.floor(bits) !== bits ||
bits > 0x207fffff ||
bits < 1 ||
(bits & 0x00800000) !== 0 ||
(bits & 0x007fffff) === 0
) {
throw new Error('Invalid bits');
}
};
verifyBits(oldBits);
verifyBits(newBits);
// No need to mask exponents because we checked the bounds above
const oldExp = oldBits >> 24;
const newExp = newBits >> 24;
const oldNum = oldBits & 0x007fffff;
const newNum = newBits & 0x007fffff;
// The diff can only possibly be 1, 0, -1
// (because maximum difficulty change is x4 or /4 (2 bits up or down))
let result: number;
switch (newExp - oldExp) {
// New less than old, target lowered, difficulty increased
case -1:
result = ((oldNum << 8) * 100) / newNum - 100;
break;
// Same exponent, compare numbers as is.
case 0:
result = (oldNum * 100) / newNum - 100;
break;
// Old less than new, target raised, difficulty decreased
case 1:
result = (oldNum * 100) / (newNum << 8) - 100;
break;
default:
throw new Error('Impossible exponent difference');
}
// Min/Max values
return result > 300 ? 300 : result < -75 ? -75 : result;
}
export function calcDifficultyAdjustment(
DATime: number,
nowSeconds: number,

View File

@@ -179,7 +179,6 @@ class DiskCache {
return;
}
try {
const start = Date.now();
let data: any = {};
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
if (cacheData) {
@@ -221,8 +220,6 @@ class DiskCache {
}
}
logger.info(`Loaded mempool from disk cache in ${Date.now() - start} ms`);
await memPool.$setMempool(data.mempool);
if (!this.ignoreBlocksCache) {
blocks.setBlocks(data.blocks);

View File

@@ -1,11 +1,10 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction } from '../../rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
import mempool from './mempool';
const MAX_UINT32 = Math.pow(2, 32) - 1;
@@ -171,7 +170,7 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = [];
let removed: string[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
const changed: { txid: string, rate: number | undefined }[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
@@ -193,8 +192,8 @@ class MempoolBlocks {
mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) {
added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
} else if (tx.rate !== prevIds[tx.txid].rate) {
changed.push({ txid: tx.txid, rate: tx.rate });
}
});
}
@@ -207,20 +206,15 @@ class MempoolBlocks {
return mempoolBlockDeltas;
}
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
if (saveResults) {
this.resetUids();
}
// set missing short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx, !saveResults);
this.setUid(tx);
}
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
@@ -228,7 +222,7 @@ class MempoolBlocks {
if (entry.uid !== null && entry.uid !== undefined) {
const stripped = {
uid: entry.uid,
fee: entry.fee + (useAccelerations && (!accelerationPool || accelerations[entry.txid]?.pools?.includes(accelerationPool)) ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
fee: entry.fee,
weight: (entry.adjustedVsize * 4),
sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@@ -268,7 +262,7 @@ class MempoolBlocks {
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, accelerationPool, saveResults);
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
@@ -279,29 +273,25 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
if (!this.txSelectionWorker) {
// need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
await this.$makeBlockTemplates(newMempool, saveResults);
return;
}
const start = Date.now();
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
for (const tx of addedAndChanged) {
for (const tx of Object.values(added)) {
this.setUid(tx, true);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
const addedStripped: CompactThreadTransaction[] = added.filter(entry => (entry.uid !== null && entry.uid !== undefined)).map(entry => {
return {
uid: entry.uid || 0,
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
fee: entry.fee,
weight: (entry.adjustedVsize * 4),
sigops: entry.sigops,
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
@@ -328,7 +318,7 @@ class MempoolBlocks {
// clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, null, saveResults);
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
} catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
@@ -340,7 +330,7 @@ class MempoolBlocks {
this.rustGbtGenerator = new GbtGenerator();
}
public async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
private async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
@@ -356,25 +346,16 @@ class MempoolBlocks {
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
}
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
const convertedAccelerations = acceleratedList.map(acc => {
return {
uid: this.getUid(newMempool[acc.txid]),
delta: acc.feeDelta,
};
});
// run the block construction algorithm in a separate thread, and wait for a result
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
try {
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], this.nextUid),
);
if (saveResults) {
this.rustInitialized = true;
}
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, saveResults);
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, saveResults);
logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
} catch (e) {
@@ -386,20 +367,20 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
return this.$rustMakeBlockTemplates(newMempool, false, useAccelerations, accelerationPool);
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> {
return this.$rustMakeBlockTemplates(newMempool, false);
}
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[]): Promise<void> {
// GBT optimization requires that uids never get too sparse
// as a sanity check, we should also explicitly prevent uint32 uid overflow
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
this.resetRustGbt();
}
if (!this.rustInitialized) {
// need to reset the worker
return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool);
await this.$rustMakeBlockTemplates(newMempool, true);
return;
}
const start = Date.now();
@@ -413,22 +394,12 @@ class MempoolBlocks {
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
const convertedAccelerations = acceleratedList.map(acc => {
return {
uid: this.getUid(newMempool[acc.txid]),
delta: acc.feeDelta,
};
});
// run the block construction algorithm in a separate thread, and wait for a result
try {
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
await this.rustGbtGenerator.update(
added as RustThreadTransaction[],
removedUids,
convertedAccelerations as RustThreadAcceleration[],
this.nextUid,
),
);
@@ -436,19 +407,17 @@ class MempoolBlocks {
if (mempoolSize !== resultMempoolSize) {
throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
} else {
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true);
this.removeUids(removedUids);
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, true);
}
this.removeUids(removedUids);
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
} catch (e) {
logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
this.resetRustGbt();
return this.mempoolBlocks;
}
}
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], saveResults): MempoolBlockWithTransactions[] {
for (const [txid, rate] of rates) {
if (txid in mempool) {
mempool[txid].effectiveFeePerVsize = rate;
@@ -499,8 +468,6 @@ class MempoolBlocks {
}
}
const isAccelerated : { [txid: string]: boolean } = {};
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
// update this thread's mempool with the results
let mempoolTx: MempoolTransactionExtended;
@@ -529,17 +496,6 @@ class MempoolBlocks {
mempoolTx.cpfpChecked = true;
}
const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
mempoolTx.acceleration = true;
for (const ancestor of mempoolTx.ancestors || []) {
mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true;
}
} else {
delete mempoolTx.acceleration;
}
// online calculation of stack-of-blocks fee stats
if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
feeStatsCalculator.processNext(mempoolTx);
@@ -576,7 +532,7 @@ class MempoolBlocks {
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
if (!feeStats) {
feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration));
feeStats = Common.calcEffectiveFeeStatistics(transactions);
}
return {
blockSize: totalSize,

View File

@@ -1,5 +1,5 @@
import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
import logger from '../logger';
import { Common } from './common';
@@ -9,7 +9,6 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import accelerationApi, { Acceleration } from './services/acceleration';
import redisCache from './redis-cache';
class Mempool {
@@ -20,11 +19,9 @@ class Mempool {
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -69,12 +66,12 @@ class Mempool {
}
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
this.mempoolChangedCallback = fn;
}
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn;
}
@@ -110,10 +107,10 @@ class Mempool {
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
}
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
this.mempoolChangedCallback(this.mempoolCache, [], []);
}
if (this.$asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []);
}
this.addToSpendMap(Object.values(this.mempoolCache));
}
@@ -126,7 +123,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
const result = await bitcoinApi.$getMempoolTransactions(last_txid);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -185,7 +182,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[], pollRate: number): Promise<void> {
public async $updateMempool(transactions: string[]): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@@ -234,40 +231,34 @@ class Mempool {
}
if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = 10000;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
logger.debug(`fetched ${txs.length} transactions`);
this.updateTimerProgress(timer, 'fetched new transactions');
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
for (const transaction of txs) {
this.mempoolCache[transaction.txid] = transaction;
if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime());
this.vBytesPerSecondArray.push({
unixTime: new Date().getTime(),
vSize: transaction.vsize,
});
}
hasChange = true;
newTransactions.push(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
} catch (e: any) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
}
}
if (txs.length < slice.length) {
const missing = slice.length - txs.length;
if (config.MEMPOOL.BACKEND === 'esplora') {
this.missingTxCount += missing;
}
logger.debug(`Error finding ${missing} transactions in the mempool: `);
}
if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) {
if (Date.now() - intervalTimer > 5_000) {
if (this.inSync) {
// Break and restart mempool loop if we spend too much time processing
// new transactions that may lead to falling behind on block height
@@ -279,7 +270,7 @@ class Mempool {
if (Math.floor(progress) < 100) {
loadingIndicators.setProgress('mempool', progress);
}
intervalTimer = Date.now();
intervalTimer = Date.now()
}
}
}
@@ -303,7 +294,7 @@ class Mempool {
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
setTimeout(() => {
this.mempoolProtection = 2;
logger.warn('Mempool clear protection ended, normal operation resumed.');
logger.warn('Mempool clear protection resumed.');
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
@@ -330,19 +321,14 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = await this.$updateAccelerations();
if (accelerationDelta.length) {
hasChange = true;
}
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions);
this.updateTimerProgress(timer, 'completed async mempool callback');
}
@@ -366,70 +352,6 @@ class Mempool {
this.clearTimer(timer);
}
public getAccelerations(): { [txid: string]: Acceleration } {
return this.accelerations;
}
public async $updateAccelerations(): Promise<string[]> {
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
return [];
}
try {
const newAccelerations = await accelerationApi.$fetchAccelerations();
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap;
return changed;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];
}
}
private startTimer() {
const state: any = {
start: Date.now(),

View File

@@ -12,7 +12,6 @@ import PricesRepository from '../../repositories/PricesRepository';
class MiningRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools', this.$listPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks)
@@ -42,10 +41,6 @@ class MiningRoutes {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
res.status(400).send('Prices are not available on testnets.');
return;
}
if (req.query.timestamp) {
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
parseInt(<string>req.query.timestamp ?? 0, 10)
@@ -93,29 +88,6 @@ class MiningRoutes {
}
}
private async $listPools(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
const pools = await mining.$listPools();
if (!pools) {
res.status(500).end();
return;
}
res.header('X-total-count', pools.length.toString());
if (pools.length === 0) {
res.status(204).send();
} else {
res.json(pools);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPools(req: Request, res: Response) {
try {
const stats = await mining.$getPoolsStats(req.params.interval);

View File

@@ -11,7 +11,7 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
import config from '../../config';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
@@ -26,7 +26,7 @@ class Mining {
/**
* Get historical blocks health
*/
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> {
return await BlocksAuditsRepository.$getBlocksHealthHistory(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -56,7 +56,7 @@ class Mining {
/**
* Get historical block fee rates percentiles
*/
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFeeRates(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -66,7 +66,7 @@ class Mining {
/**
* Get historical block sizes
*/
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockSizes(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -76,7 +76,7 @@ class Mining {
/**
* Get historical block weights
*/
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockWeights(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
@@ -107,7 +107,6 @@ class Mining {
slug: poolInfo.slug,
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
avgFeeDelta: poolInfo.avgFeeDelta,
poolUniqueId: poolInfo.poolUniqueId
};
poolsStats.push(poolStat);
});
@@ -202,7 +201,7 @@ class Mining {
try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
@@ -313,7 +312,7 @@ class Mining {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try {
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date());
@@ -422,9 +421,8 @@ class Mining {
}
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty;
let currentBits = genesisBlock.bits;
let totalIndexed = 0;
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
@@ -438,7 +436,6 @@ class Mining {
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
}
@@ -446,11 +443,10 @@ class Mining {
let timer = new Date().getTime() / 1000;
for (const block of blocks) {
if (block.bits !== currentBits) {
if (block.difficulty !== currentDifficulty) {
if (indexedHeights[block.height] === true) { // Already indexed
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
continue;
}
@@ -468,7 +464,6 @@ class Mining {
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
}
@@ -595,20 +590,6 @@ class Mining {
}
}
/**
* List existing mining pools
*/
public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> {
const [rows] = await database.query(`
SELECT
name,
slug,
unique_id
FROM pools`
);
return rows as {name: string, slug: string, unique_id: number}[];
}
private getDateMidnight(date: Date): Date {
date.setUTCHours(0);
date.setUTCMinutes(0);

View File

@@ -1,19 +0,0 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
class PricesRoutes {
public initRoutes(app: Application): void {
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this));
}
private $getCurrentPrices(req: Request, res: Response): void {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
res.json(pricesUpdater.getLatestPrices());
}
}
export default new PricesRoutes();

View File

@@ -53,7 +53,7 @@ class RbfCache {
private expiring: Map<string, number> = new Map();
private cacheQueue: CacheEvent[] = [];
public init(): void {
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
@@ -146,24 +146,6 @@ class RbfCache {
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
}
public has(txId: string): boolean {
return this.txs.has(txId);
}
public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean {
const tree = this.getRbfTree(txId);
if (!tree) {
return false;
}
const txs = this.getTransactionsInTree(tree);
for (const tx of txs) {
if (predicate(tx)) {
return true;
}
}
return false;
}
public getReplacedBy(txId: string): string | undefined {
return this.replacedBy.get(txId);
}
@@ -360,14 +342,14 @@ class RbfCache {
public async load({ txs, trees, expiring }): Promise<void> {
txs.forEach(txEntry => {
this.txs.set(txEntry.key, txEntry.value);
this.txs.set(txEntry[0], txEntry[1]);
});
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry.key)) {
this.expiring.set(expiringEntry.key, new Date(expiringEntry.value).getTime());
if (this.txs.has(expiringEntry[0])) {
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
}
});
this.cleanup();

View File

@@ -23,21 +23,31 @@ class RedisCache {
private cacheQueue: MempoolTransactionExtended[] = [];
private txFlushLimit: number = 10000;
constructor() {
if (config.REDIS.ENABLED) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
this.$ensureConnected();
this.client.exists('mempool:0').then((mempoolExists) => {
if (!mempoolExists) {
for (let i = 0; i < 16; i++) {
this.client.json.set(`mempool:${i.toString(16)}`, '$', {});
}
}
});
}
}
private async $ensureConnected(): Promise<void> {
if (!this.connected && config.REDIS.ENABLED) {
if (!this.client) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
}
return this.client.connect().then(async () => {
this.connected = true;
logger.info(`Redis client connected`);
@@ -55,8 +65,7 @@ class RedisCache {
async $updateBlocks(blocks: BlockExtended[]) {
try {
await this.$ensureConnected();
await this.client.set('blocks', JSON.stringify(blocks));
logger.debug(`Saved latest blocks to Redis cache`);
await this.client.json.set('blocks', '$', blocks);
} catch (e) {
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
}
@@ -65,10 +74,9 @@ class RedisCache {
async $updateBlockSummaries(summaries: BlockSummary[]) {
try {
await this.$ensureConnected();
await this.client.set('block-summaries', JSON.stringify(summaries));
logger.debug(`Saved latest block summaries to Redis cache`);
await this.client.json.set('block-summaries', '$', summaries);
} catch (e) {
logger.warn(`Failed to update block summaries in Redis cache: ${e instanceof Error ? e.message : e}`);
logger.warn(`Failed to update blocks in Redis cache: ${e instanceof Error ? e.message : e}`);
}
}
@@ -82,20 +90,17 @@ class RedisCache {
async $flushTransactions() {
const success = await this.$addTransactions(this.cacheQueue);
if (success) {
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
logger.info(`Flushed ${this.cacheQueue.length} transactions to Redis cache`);
this.cacheQueue = [];
} else {
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
logger.err(`Failed to flush ${this.cacheQueue.length} transactions to Redis cache`);
}
}
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
if (!newTransactions.length) {
return true;
}
try {
await this.$ensureConnected();
const msetData = newTransactions.map(tx => {
await Promise.all(newTransactions.map(tx => {
const minified: any = { ...tx };
delete minified.hex;
for (const vin of minified.vin) {
@@ -106,9 +111,8 @@ class RedisCache {
for (const vout of minified.vout) {
delete vout.scriptpubkey_asm;
}
return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
});
await this.client.MSET(msetData);
return this.client.json.set(`mempool:${tx.txid.slice(0,1)}`, tx.txid, minified);
}));
return true;
} catch (e) {
logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
@@ -119,11 +123,9 @@ class RedisCache {
async $removeTransactions(transactions: string[]) {
try {
await this.$ensureConnected();
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
}
await Promise.all(transactions.map(txid => {
return this.client.json.del(`mempool:${txid.slice(0,1)}`, txid);
}));
} catch (e) {
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
}
@@ -132,7 +134,7 @@ class RedisCache {
async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
try {
await this.$ensureConnected();
await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
await this.client.json.set(`rbf:${type}:${txid}`, '$', value);
} catch (e) {
logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
}
@@ -141,7 +143,7 @@ class RedisCache {
async $removeRbfEntry(type: string, txid: string): Promise<void> {
try {
await this.$ensureConnected();
await this.client.unlink(`rbf:${type}:${txid}`);
await this.client.del(`rbf:${type}:${txid}`);
} catch (e) {
logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
}
@@ -150,8 +152,7 @@ class RedisCache {
async $getBlocks(): Promise<BlockExtended[]> {
try {
await this.$ensureConnected();
const json = await this.client.get('blocks');
return JSON.parse(json);
return this.client.json.get('blocks');
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
@@ -161,8 +162,7 @@ class RedisCache {
async $getBlockSummaries(): Promise<BlockSummary[]> {
try {
await this.$ensureConnected();
const json = await this.client.get('block-summaries');
return JSON.parse(json);
return this.client.json.get('block-summaries');
} catch (e) {
logger.warn(`Failed to retrieve blocks from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
@@ -171,14 +171,16 @@ class RedisCache {
async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
const start = Date.now();
const mempool = {};
let mempool = {};
try {
await this.$ensureConnected();
const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
for (const tx of mempoolList) {
mempool[tx.key] = tx.value;
for (let i = 0; i < 16; i++) {
const shard = await this.client.json.get(`mempool:${i.toString(16)}`);
logger.info(`Loaded ${Object.keys(shard).length} transactions from redis cache ${i.toString(16)}`);
mempool = Object.assign(mempool, shard);
}
logger.info(`Loaded mempool from Redis cache in ${Date.now() - start} ms`);
logger.info(`Total ${Object.keys(mempool).length} transactions loaded from redis cache `);
logger.info(`Loaded redis cache in ${Date.now() - start} ms`);
return mempool || {};
} catch (e) {
logger.warn(`Failed to retrieve mempool from Redis cache: ${e instanceof Error ? e.message : e}`);
@@ -189,8 +191,17 @@ class RedisCache {
async $getRbfEntries(type: string): Promise<any[]> {
try {
await this.$ensureConnected();
const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
return rbfEntries;
const keys = await this.client.keys(`rbf:${type}:*`);
const promises: Promise<MempoolTransactionExtended[]>[] = [];
for (let i = 0; i < keys.length; i += 10000) {
const keySlice = keys.slice(i, i + 10000);
if (!keySlice.length) {
continue;
}
promises.push(this.client.json.mGet(keySlice, '$').then(chunk => chunk?.length ? chunk.flat().map((v, i) => [keySlice[i].slice(`rbf:${type}:`.length), v]) : [] ));
}
const entries = await Promise.all(promises);
return entries.flat();
} catch (e) {
logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: ${e instanceof Error ? e.message : e}`);
return [];
@@ -216,7 +227,7 @@ class RedisCache {
await memPool.$setMempool(loadedMempool);
await rbfCache.load({
txs: rbfTxs,
trees: rbfTrees.map(loadedTree => loadedTree.value),
trees: rbfTrees.map(loadedTree => loadedTree[1]),
expiring: rbfExpirations,
});
}
@@ -237,37 +248,6 @@ class RedisCache {
}
}
private async scanKeys<T>(pattern): Promise<{ key: string, value: T }[]> {
logger.info(`loading Redis entries for ${pattern}`);
let keys: string[] = [];
const result: { key: string, value: T }[] = [];
const patternLength = pattern.length - 1;
let count = 0;
const processValues = async (keys): Promise<void> => {
const values = await this.client.MGET(keys);
for (let i = 0; i < values.length; i++) {
if (values[i]) {
result.push({ key: keys[i].slice(patternLength), value: JSON.parse(values[i]) });
count++;
}
}
logger.info(`loaded ${count} entries from Redis cache`);
};
for await (const key of this.client.scanIterator({
MATCH: pattern,
COUNT: 100
})) {
keys.push(key);
if (keys.length >= 10000) {
await processValues(keys);
keys = [];
}
}
if (keys.length) {
await processValues(keys);
}
return result;
}
}
export default new RedisCache();

View File

@@ -1,30 +0,0 @@
import { query } from '../../utils/axios-query';
import config from '../../config';
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
export interface Acceleration {
txid: string,
feeDelta: number,
pools: number[],
}
class AccelerationApi {
public async $fetchAccelerations(): Promise<Acceleration[]> {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`);
return (response as Acceleration[]) || [];
} else {
return [];
}
}
public isAcceleratedBlock(block: BlockExtended, accelerations: Acceleration[]): boolean {
let anyAccelerated = false;
for (let i = 0; i < accelerations.length && !anyAccelerated; i++) {
anyAccelerated = anyAccelerated || accelerations[i].pools?.includes(block.extras.pool.id);
}
return anyAccelerated;
}
}
export default new AccelerationApi();

View File

@@ -3,8 +3,6 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger';
import config from '../config';
class TransactionUtils {
constructor() { }
@@ -24,23 +22,6 @@ class TransactionUtils {
};
}
// Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
// Propagates any error from the retry request.
public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
try {
const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
if (result) {
return result;
} else {
logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
}
} catch (e) {
logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
}
// retry direct from Core if first request failed
return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
}
/**
* @param txId
* @param addPrevouts
@@ -50,7 +31,7 @@ class TransactionUtils {
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise<TransactionExtended> {
let transaction: IEsploraApi.Transaction;
if (forceCore === true) {
transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
} else {
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
}
@@ -72,24 +53,6 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended;
}
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const results = await Promise.allSettled(txids.map(txid => this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, true)));
return (results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<MempoolTransactionExtended>[]).map(r => r.value);
} else {
const transactions = await bitcoinApi.$getMempoolTransactions(txids);
return transactions.map(transaction => {
if (Common.isLiquid()) {
if (!isFinite(Number(transaction.fee))) {
transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0);
}
}
return this.extendMempoolTransaction(transaction);
});
}
}
public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {

View File

@@ -21,8 +21,6 @@ import Audit from './audit';
import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
// valid 'want' subscriptions
const wantable = [
@@ -174,15 +172,9 @@ class WebsocketHandler {
}
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
const position: { block: number, vsize: number, accelerated?: boolean } = {
...tx.position
};
if (tx.acceleration) {
position.accelerated = tx.acceleration;
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position
position: tx.position,
});
}
} else {
@@ -191,19 +183,13 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
.test(parsedMessage['track-address'])) {
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
client['track-address'] = '41' + matchedAddress + 'ac';
} else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
client['track-address'] = '21' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
}
client['track-address'] = matchedAddress;
} else {
client['track-address'] = null;
}
@@ -394,7 +380,7 @@ class WebsocketHandler {
}
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
if (!this.wss) {
throw new Error('WebSocket.Server is not set');
}
@@ -403,9 +389,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions);
} else {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
}
} else {
mempoolBlocks.updateMempoolBlocks(newMempool, true);
@@ -484,9 +470,6 @@ class WebsocketHandler {
}
}
// pre-compute address transactions
const addressCache = this.makeAddressCache(newTransactions);
this.wss.clients.forEach(async (client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -526,13 +509,40 @@ class WebsocketHandler {
}
if (client['track-address']) {
const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []);
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions;
const foundTransactions: TransactionExtended[] = [];
if (fullTransactions.length) {
response['address-transactions'] = JSON.stringify(fullTransactions);
for (const tx of newTransactions) {
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
foundTransactions.push(fullTx);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
foundTransactions.push(tx);
}
}
}
if (foundTransactions.length) {
response['address-transactions'] = JSON.stringify(foundTransactions);
}
}
@@ -540,6 +550,7 @@ class WebsocketHandler {
const foundTransactions: TransactionExtended[] = [];
newTransactions.forEach((tx) => {
if (client['track-asset'] === Common.nativeAssetId) {
if (tx.vin.some((vin) => !!vin.is_pegin)) {
foundTransactions.push(tx);
@@ -588,10 +599,7 @@ class WebsocketHandler {
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
position: mempoolTx.position,
});
}
}
@@ -639,7 +647,6 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks;
let auditMempool = _memPool;
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
// template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
@@ -647,27 +654,19 @@ class WebsocketHandler {
auditMempool = deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
if (config.MEMPOOL.RUST_GBT) {
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
}
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
} else {
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
if (config.MEMPOOL.RUST_GBT) {
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id);
} else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
}
} else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
}
if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@@ -696,7 +695,6 @@ class WebsocketHandler {
freshTxs: fresh,
sigopTxs: sigop,
fullrbfTxs: fullrbf,
acceleratedTxs: accelerated,
matchRate: matchRate,
expectedFees: totalFees,
expectedWeight: totalWeight,
@@ -724,9 +722,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions, true);
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions);
} else {
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
}
} else {
mempoolBlocks.updateMempoolBlocks(_memPool, true);
@@ -738,9 +736,6 @@ class WebsocketHandler {
const fees = feeApi.getRecommendedFee();
const mempoolInfo = memPool.getMempoolInfo();
// pre-compute address transactions
const addressCache = this.makeAddressCache(transactions);
// update init data
this.updateSocketDataFields({
'mempoolInfo': mempoolInfo,
@@ -793,17 +788,24 @@ class WebsocketHandler {
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
position: mempoolTx.position,
});
}
}
}
if (client['track-address']) {
const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []);
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
return;
}
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
}
});
if (foundTransactions.length) {
foundTransactions.forEach((tx) => {
@@ -881,52 +883,6 @@ class WebsocketHandler {
+ '}';
}
private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
for (const tx of transactions) {
for (const vin of tx.vin) {
if (vin?.prevout?.scriptpubkey_address) {
if (!addressCache[vin.prevout.scriptpubkey_address]) {
addressCache[vin.prevout.scriptpubkey_address] = new Set();
}
addressCache[vin.prevout.scriptpubkey_address].add(tx);
}
if (vin?.prevout?.scriptpubkey) {
if (!addressCache[vin.prevout.scriptpubkey]) {
addressCache[vin.prevout.scriptpubkey] = new Set();
}
addressCache[vin.prevout.scriptpubkey].add(tx);
}
}
for (const vout of tx.vout) {
if (vout?.scriptpubkey_address) {
if (!addressCache[vout?.scriptpubkey_address]) {
addressCache[vout?.scriptpubkey_address] = new Set();
}
addressCache[vout?.scriptpubkey_address].add(tx);
}
if (vout?.scriptpubkey) {
if (!addressCache[vout.scriptpubkey]) {
addressCache[vout.scriptpubkey] = new Set();
}
addressCache[vout.scriptpubkey].add(tx);
}
}
}
return addressCache;
}
private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise<MempoolTransactionExtended[]> {
for (let i = 0; i < transactions.length; i++) {
try {
transactions[i] = await transactionUtils.$getMempoolTransactionExtended(transactions[i].txid, true);
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
}
return transactions;
}
private printLogs(): void {
if (this.wss) {
const count = this.wss?.clients?.size || 0;

View File

@@ -38,13 +38,11 @@ interface IConfig {
DISK_CACHE_BLOCK_INTERVAL: number;
MAX_PUSH_TX_SIZE_WEIGHT: number;
ALLOW_UNREACHABLE: boolean;
PRICE_UPDATES_PER_HOUR: number;
};
ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
RETRY_UNIX_SOCKET_AFTER: number;
FALLBACK: string[];
};
LIGHTNING: {
ENABLED: boolean;
@@ -117,6 +115,10 @@ interface IConfig {
USERNAME: string;
PASSWORD: string;
};
PRICE_DATA_SERVER: {
TOR_URL: string;
CLEARNET_URL: string;
};
EXTERNAL_DATA_SERVER: {
MEMPOOL_API: string;
MEMPOOL_ONION: string;
@@ -137,10 +139,6 @@ interface IConfig {
AUDIT_START_HEIGHT: number;
SERVERS: string[];
},
MEMPOOL_SERVICES: {
API: string;
ACCELERATIONS: boolean;
},
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
@@ -183,13 +181,11 @@ const defaults: IConfig = {
'DISK_CACHE_BLOCK_INTERVAL': 6,
'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
'ALLOW_UNREACHABLE': true,
'PRICE_UPDATES_PER_HOUR': 1,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'RETRY_UNIX_SOCKET_AFTER': 30000,
'FALLBACK': [],
},
'ELECTRUM': {
'HOST': '127.0.0.1',
@@ -262,6 +258,10 @@ const defaults: IConfig = {
'USERNAME': '',
'PASSWORD': ''
},
'PRICE_DATA_SERVER': {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
'EXTERNAL_DATA_SERVER': {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@@ -282,10 +282,6 @@ const defaults: IConfig = {
'AUDIT_START_HEIGHT': 774000,
'SERVERS': [],
},
'MEMPOOL_SERVICES': {
'API': '',
'ACCELERATIONS': false,
},
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
@@ -306,10 +302,10 @@ class Config implements IConfig {
LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
REPLICATION: IConfig['REPLICATION'];
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
constructor() {
@@ -327,10 +323,10 @@ class Config implements IConfig {
this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
this.REPLICATION = configs.REPLICATION;
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
}

View File

@@ -1,33 +0,0 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocks from '../api/blocks';
import config from '../config';
import transactionUtils from '../api/transaction-utils';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
if (parentPort) {
parentPort.on('message', async ({ hash, height }) => {
if (hash != null && height != null) {
await indexBlockSummary(hash, height);
}
if (parentPort) {
parentPort.postMessage(height);
}
});
}
async function indexBlockSummary(hash: string, height: number): Promise<void> {
const block = await bitcoinClient.getBlock(hash, 2);
const txs = block.tx.map(tx => {
tx.fee = Math.round(tx.fee * 100_000_000);
tx.vout.forEach((vout) => {
vout.value = Math.round(vout.value * 100000000);
});
tx.vsize = Math.round(tx.weight / 4); // required for backwards compatibility
return tx;
});
const cpfpSummary = await blocks.$indexCPFP(hash, height, txs);
await blocks.$getStrippedBlockTransactions(hash, true, true, cpfpSummary, height); // This will index the block summary
}

View File

@@ -1,25 +0,0 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocksRepository from '../repositories/BlocksRepository';
import blocks from '../api/blocks';
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
if (parentPort) {
parentPort.on('message', async (params) => {
if (params.height != null) {
await indexBlock(params.height);
}
if (parentPort) {
parentPort.postMessage(params.height);
}
});
}
async function indexBlock(blockHeight: number): Promise<void> {
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await blocks['$getTransactionsExtended'](blockHash, block.height, true, null, true);
const blockExtended = await blocks['$getBlockExtended'](block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}

View File

@@ -30,7 +30,6 @@ import generalLightningRoutes from './api/explorer/general.routes';
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes';
import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes';
@@ -43,7 +42,6 @@ import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache';
import rbfCache from './api/rbf-cache';
class Server {
private wss: WebSocket.Server | undefined;
@@ -92,10 +90,6 @@ class Server {
async startServer(worker = false): Promise<void> {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks();
}
if (config.DATABASE.ENABLED) {
await DB.checkDbConnection();
try {
@@ -108,8 +102,6 @@ class Server {
}
}
rbfCache.init();
this.app
.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*');
@@ -196,16 +188,14 @@ class Server {
}
const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate);
await memPool.$updateMempool(newMempool);
}
indexer.$run();
priceUpdater.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
const elapsed = Date.now() - start;
const remainingTime = Math.max(0, pollRate - elapsed);
const remainingTime = Math.max(0, config.MEMPOOL.POLL_RATE_MS - elapsed)
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
this.backendRetryCount = 0;
} catch (e: any) {
@@ -270,7 +260,6 @@ class Server {
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app);
}

View File

@@ -105,12 +105,6 @@ class Indexer {
return;
}
try {
await priceUpdater.$run();
} catch (e) {
logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e));
}
// Do not attempt to index anything unless Bitcoin Core is fully synced
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) {
@@ -125,6 +119,8 @@ class Indexer {
await this.checkAvailableCoreIndexes();
try {
await priceUpdater.$run();
const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration

View File

@@ -20,7 +20,6 @@ export interface PoolInfo {
slug: string;
avgMatchRate: number | null;
avgFeeDelta: number | null;
poolUniqueId: number;
}
export interface PoolStats extends PoolInfo {
@@ -37,7 +36,6 @@ export interface BlockAudit {
sigopTxs: string[],
fullrbfTxs: string[],
addedTxs: string[],
acceleratedTxs: string[],
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
@@ -93,7 +91,6 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
block: number,
vsize: number,
};
acceleration?: boolean;
uid?: number;
}
@@ -185,7 +182,6 @@ export interface TransactionStripped {
fee: number;
vsize: number;
value: number;
acc?: boolean;
rate?: number; // effective fee rate
}

View File

@@ -116,7 +116,6 @@ class AuditReplication {
freshTxs: auditSummary.freshTxs || [],
sigopTxs: auditSummary.sigopTxs || [],
fullrbfTxs: auditSummary.fullrbfTxs || [],
acceleratedTxs: auditSummary.acceleratedTxs || [],
matchRate: auditSummary.matchRate,
expectedFees: auditSummary.expectedFees,
expectedWeight: auditSummary.expectedWeight,

View File

@@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
@@ -69,7 +69,6 @@ class BlocksAuditRepositories {
fresh_txs as freshTxs,
sigop_txs as sigopTxs,
fullrbf_txs as fullrbfTxs,
accelerated_txs as acceleratedTxs,
match_rate as matchRate,
expected_fees as expectedFees,
expected_weight as expectedWeight
@@ -84,7 +83,6 @@ class BlocksAuditRepositories {
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
rows[0].template = JSON.parse(rows[0].template);
return rows[0];

View File

@@ -1,4 +1,3 @@
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces';
import DB from '../database';
import logger from '../logger';
@@ -13,7 +12,6 @@ import config from '../config';
import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
interface DatabaseBlock {
id: string;
@@ -541,7 +539,7 @@ class BlocksRepository {
*/
public async $getBlocksDifficulty(): Promise<object[]> {
try {
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`);
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
return rows;
} catch (e) {
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));
@@ -850,7 +848,7 @@ class BlocksRepository {
*/
public async $getOldestConsecutiveBlock(): Promise<any> {
try {
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, bits FROM blocks ORDER BY height DESC`);
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
for (let i = 0; i < rows.length - 1; ++i) {
if (rows[i].height - rows[i + 1].height > 1) {
return rows[i];
@@ -1038,17 +1036,8 @@ class BlocksRepository {
{
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
if (extras.feePercentiles === null) {
let summary;
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
} else {
// Call Core RPC
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
summary = blocks.summarizeBlock(block);
}
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
}

View File

@@ -40,8 +40,7 @@ class PoolsRepository {
pools.link AS link,
slug,
AVG(blocks_audits.match_rate) AS avgMatchRate,
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta,
unique_id as poolUniqueId
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta
FROM blocks
JOIN pools on pools.id = pool_id
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height

View File

@@ -25,10 +25,7 @@ export interface PriceHistory {
class PriceUpdater {
public historyInserted = false;
private timeBetweenUpdatesMs = 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
private cyclePosition = -1;
private firstRun = true;
private lastTime = -1;
private lastRun = 0;
private lastHistoricalRun = 0;
private running = false;
private feeds: PriceFeed[] = [];
@@ -44,8 +41,6 @@ class PriceUpdater {
this.feeds.push(new CoinbaseApi());
this.feeds.push(new BitfinexApi());
this.feeds.push(new GeminiApi());
this.setCyclePosition();
}
public getLatestPrices(): ApiPrice {
@@ -105,48 +100,22 @@ class PriceUpdater {
this.running = false;
}
private getMillisecondsSinceBeginningOfHour(): number {
const now = new Date();
const beginningOfHour = new Date(now);
beginningOfHour.setMinutes(0, 0, 0);
return now.getTime() - beginningOfHour.getTime();
}
private setCyclePosition(): void {
const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour();
for (let i = 0; i < config.MEMPOOL.PRICE_UPDATES_PER_HOUR; i++) {
if (this.timeBetweenUpdatesMs * i > millisecondsSinceBeginningOfHour) {
this.cyclePosition = i;
return;
}
}
this.cyclePosition = config.MEMPOOL.PRICE_UPDATES_PER_HOUR;
}
/**
* Fetch last BTC price from exchanges, average them, and save it in the database once every hour
*/
private async $updatePrice(): Promise<void> {
let forceUpdate = false;
if (this.firstRun === true && config.DATABASE.ENABLED === true) {
const lastUpdate = await PricesRepository.$getLatestPriceTime();
if (new Date().getTime() / 1000 - lastUpdate > this.timeBetweenUpdatesMs / 1000) {
forceUpdate = true;
}
this.firstRun = false;
if (this.lastRun === 0 && config.DATABASE.ENABLED === true) {
this.lastRun = await PricesRepository.$getLatestPriceTime();
}
const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour();
// Reset the cycle on new hour
if (this.lastTime > millisecondsSinceBeginningOfHour) {
this.cyclePosition = 0;
}
this.lastTime = millisecondsSinceBeginningOfHour;
if (millisecondsSinceBeginningOfHour < this.timeBetweenUpdatesMs * this.cyclePosition && !forceUpdate && this.cyclePosition !== 0) {
if ((Math.round(new Date().getTime() / 1000) - this.lastRun) < 3600) {
// Refresh only once every hour
return;
}
const previousRun = this.lastRun;
this.lastRun = new Date().getTime() / 1000;
for (const currency of this.currencies) {
let prices: number[] = [];
@@ -177,27 +146,26 @@ class PriceUpdater {
}
}
if (config.DATABASE.ENABLED === true && this.cyclePosition === 0) {
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (config.DATABASE.ENABLED === true) {
// Save everything in db
try {
const p = 60 * 60 * 1000; // milliseconds in an hour
const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
this.latestPrices.time = nowRounded.getTime() / 1000;
await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices);
} catch (e) {
this.lastRun = previousRun + 5 * 60;
logger.err(`Cannot save latest prices into db. Trying again in 5 minutes. Reason: ${(e instanceof Error ? e.message : e)}`);
}
}
this.latestPrices.time = Math.round(new Date().getTime() / 1000);
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.latestPrices);
}
if (!forceUpdate) {
this.cyclePosition++;
}
this.lastRun = new Date().getTime() / 1000;
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
Signed: Czino

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file with sha256 hash c80c5ee4c71c5a76a1f6cd35339bd0c45b25b491933ea7b02a66470e9f43a6fd.
Signed: TheBlueMatt

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of August 2, 2023.
Signed: andrewtoth

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: bguillaumat

View File

@@ -1,5 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
Signed: fiatjaf

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
Signed: rishkwal

View File

@@ -113,8 +113,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6,
"PRICE_UPDATES_PER_HOUR": 1
"DISK_CACHE_BLOCK_INTERVAL": 6
},
```
@@ -147,7 +146,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
MEMPOOL_PRICE_UPDATES_PER_HOUR: ""
...
```
@@ -365,6 +363,25 @@ Corresponding `docker-compose.yml` overrides:
<br/>
`mempool-config.json`:
```json
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
PRICE_DATA_SERVER_TOR_URL: ""
PRICE_DATA_SERVER_CLEARNET_URL: ""
...
```
<br/>
`mempool-config.json`:
```json
"LIGHTNING": {

View File

@@ -8,7 +8,6 @@
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CACHE_ENABLED": __MEMPOOL_CACHE_ENABLED__,
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
@@ -33,8 +32,7 @@
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__"
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
@@ -51,8 +49,7 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"FALLBACK": __ESPLORA_FALLBACK__
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
@@ -113,6 +110,10 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@@ -132,13 +133,5 @@
"AUDIT": __REPLICATION_AUDIT__,
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
"SERVERS": __REPLICATION_SERVERS__
},
"MEMPOOL_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
}
}

View File

@@ -9,7 +9,6 @@ __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
__MEMPOOL_CACHE_ENABLED__=${MEMPOOL_CACHE_ENABLED:=true}
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
@@ -35,7 +34,7 @@ __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
__MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -53,7 +52,6 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -95,6 +93,10 @@ __SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
__SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
# PRICE_DATA_SERVER
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
# EXTERNAL_DATA_SERVER
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
@@ -134,13 +136,6 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -152,7 +147,6 @@ sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g"
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_ENABLED__!${__MEMPOOL_CACHE_ENABLED__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CLEAR_PROTECTION_MINUTES__!${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__!${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_BLOCK_WEIGHT_UNITS__!${__MEMPOOL_BLOCK_WEIGHT_UNITS__}!g" mempool-config.json
@@ -171,14 +165,13 @@ sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-co
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_GBT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
@@ -193,7 +186,6 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json
@@ -229,6 +221,9 @@ sed -i "s!__SOCKS5PROXY_PORT__!${__SOCKS5PROXY_PORT__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
@@ -267,12 +262,4 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
# MEMPOOL_SERVICES
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -18,7 +18,7 @@ fi
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}

View File

@@ -42,6 +42,9 @@
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
'use strict'
import 'cypress-wait-until';
import { PageIdleDetector } from './PageIdleDetector';
import { mockWebSocket } from './websocket';

View File

@@ -14,7 +14,6 @@
// ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax
import 'cypress-wait-until';
import './commands';
import failOnConsoleError from 'cypress-fail-on-console-error';

View File

@@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"types": ["cypress", "node", "cypress-wait-until"],
"types": ["cypress"],
"lib": ["es2015", "dom"],
"allowJs": true,
"noEmit": true,

View File

@@ -22,6 +22,5 @@
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
"LIGHTNING": false,
"HISTORICAL_PRICE": true,
"ACCELERATOR": false
"HISTORICAL_PRICE": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -61,18 +61,18 @@
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^16.2.0",
"@angular/animations": "^16.2.2",
"@angular/cli": "^16.2.0",
"@angular/common": "^16.2.2",
"@angular/compiler": "^16.2.2",
"@angular/core": "^16.2.2",
"@angular/forms": "^16.2.2",
"@angular/localize": "^16.2.2",
"@angular/platform-browser": "^16.2.2",
"@angular/platform-browser-dynamic": "^16.2.2",
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@angular-devkit/build-angular": "^16.1.4",
"@angular/animations": "^16.1.5",
"@angular/cli": "^16.1.4",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/localize": "^16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0",
"@fortawesome/fontawesome-svg-core": "~6.4.0",
@@ -110,10 +110,9 @@
},
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^12.17.2",
"cypress": "^12.17.1",
"cypress-fail-on-console-error": "~4.0.3",
"cypress-wait-until": "^2.0.0",
"cypress-wait-until": "^1.7.2",
"mock-socket": "~9.2.1",
"start-server-and-test": "~2.0.0"
},

View File

@@ -47,7 +47,6 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -63,7 +62,6 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
}

View File

@@ -82,7 +82,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
)
.subscribe((block: BisqBlock) => {
if (!block) {
this.seoService.logSoft404();
return;
}
this.isLoading = false;
@@ -98,7 +97,6 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
caughtHttpError(err: HttpErrorResponse){
this.error = err;
this.seoService.logSoft404();
return of(null);
}
}

View File

@@ -70,13 +70,11 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
catchError((txError: HttpErrorResponse) => {
console.log(txError);
this.error = txError;
this.seoService.logSoft404();
return of(null);
})
);
}
this.error = bisqTxError;
this.seoService.logSoft404();
return of(null);
})
);
@@ -105,7 +103,6 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
this.isLoadingTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}

View File

@@ -31,14 +31,6 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="false && officialMempoolSpace">
<h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
</div>
</ng-container>
<div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper">
@@ -181,44 +173,34 @@
</svg>
<span>Exodus</span>
</a>
<a href="https://www.luminex.io" target="_blank" title="Luminex">
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
<defs>
<style>
.lum-cls-1 {
fill: #f2ea25;
}
</style>
</defs>
<path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
<path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
<rect class="lum-cls-1" width="60.69" height="372.67"/>
</svg>
<span>Luminex</span>
</a>
</div>
</div>
<ng-container *ngIf="officialMempoolSpace">
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper">
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
</div>
</div>
<div class="community-sponsor" id="community-sponsors">
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.chads.length > 0">
<h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3>
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
</div>
</div>
</ng-container>
<div class="community-sponsor" style="margin-bottom: 68px">
<h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3>
<div class="wrapper">
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
</a>
</ng-template>
</ng-container>
</div>
</div>
@@ -358,7 +340,7 @@
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" />
</a>
</ng-template>
</div>
@@ -372,7 +354,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -384,7 +366,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -429,7 +411,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, mempool.space&trade;, the mempool logo&reg;, the mempool.space logos&trade;, the mempool square logo&reg;, and the mempool blocks logo&trade; 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&trade;, mempool.space&trade;, the mempool logo&reg;, the mempool.space logos&trade;, the mempool square logo&reg;, and the mempool blocks logo&trade; are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -10,9 +10,6 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;

View File

@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common';
@@ -19,16 +19,14 @@ import { DOCUMENT } from '@angular/common';
export class AboutComponent implements OnInit {
@ViewChild('promoVideo') promoVideo: ElementRef;
backendInfo$: Observable<IBackendInfo>;
sponsors$: Observable<any>;
translators$: Observable<ITranslators>;
allContributors$: Observable<any>;
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
showNavigateToSponsor = false;
profiles$: Observable<any>;
translators$: Observable<ITranslators>;
allContributors$: Observable<any>;
ogs$: Observable<any>;
constructor(
private websocketService: WebsocketService,
private seoService: SeoService,
@@ -45,13 +43,10 @@ export class AboutComponent implements OnInit {
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.websocketService.want(['blocks']);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
tap(() => {
this.goToAnchor()
}),
share(),
)
this.sponsors$ = this.apiService.getDonation$()
.pipe(
tap(() => this.goToAnchor())
);
this.translators$ = this.apiService.getTranslators$()
.pipe(
map((translators) => {
@@ -64,9 +59,6 @@ export class AboutComponent implements OnInit {
}),
tap(() => this.goToAnchor())
);
this.ogs$ = this.apiService.getOgs$();
this.allContributors$ = this.apiService.getContributor$().pipe(
map((contributors) => {
return {

View File

@@ -64,12 +64,12 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.address = null;
this.addressInfo = null;
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
return (this.addressString.match(/[a-f0-9]{130}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(

View File

@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addressInfo = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
@@ -84,14 +84,13 @@ export class AddressComponent implements OnInit, OnDestroy {
)
.pipe(
switchMap(() => (
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
this.addressString.match(/[a-f0-9]{130}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -119,7 +118,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
return address.is_pubkey
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address);
}),
switchMap((transactions) => {
@@ -163,13 +162,35 @@ export class AddressComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
this.stateService.mempoolTransactions$
.subscribe(tx => {
this.addTransaction(tx);
.subscribe((transaction) => {
if (this.transactions.some((t) => t.txid === transaction.txid)) {
return;
}
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
transaction.vin.forEach((vin) => {
if (vin.prevout.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
});
this.stateService.blockTransactions$
@@ -179,47 +200,12 @@ export class AddressComponent implements OnInit, OnDestroy {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
} else {
if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic');
}
}
this.totalConfirmedTxCount++;
this.loadedConfirmedTxCount++;
});
}
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
if (this.transactions.some((t) => t.txid === transaction.txid)) {
return false;
}
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (playSound) {
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
}
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
return true;
}
loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
return;

View File

@@ -86,7 +86,6 @@ export class AssetComponent implements OnInit, OnDestroy {
catchError((err) => {
this.isLoadingAsset = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
@@ -154,7 +153,6 @@ export class AssetComponent implements OnInit, OnDestroy {
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAsset = false;
});

View File

@@ -25,8 +25,7 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,8 +25,7 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,8 +25,7 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -70,11 +70,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
}
this.resizeCanvas();
}
ngOnChanges(changes): void {
@@ -149,7 +147,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
@@ -197,16 +195,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
this.gl = null;
}
handleContextRestored(event): void {
if (this.canvas?.nativeElement) {
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
}
}
this.initCanvas();
}
@HostListener('window:resize', ['$event'])
@@ -232,9 +224,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
compileShader(src, type): WebGLShader {
if (!this.gl) {
return;
}
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
@@ -248,9 +237,6 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
buildShaderProgram(shaderInfo): WebGLProgram {
if (!this.gl) {
return;
}
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
@@ -287,7 +273,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
now = performance.now();
}
// skip re-render if there's no change to the scene
if (this.scene && this.gl) {
if (this.scene) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);

View File

@@ -150,7 +150,7 @@ export default class BlockScene {
this.updateAll(startTime, 200, direction);
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now();
const removed = this.removeBatch(remove, startTime, direction);
@@ -175,7 +175,6 @@ export default class BlockScene {
// update effective rates
change.forEach(tx => {
if (this.txs[tx.txid]) {
this.txs[tx.txid].acc = tx.acc;
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
this.txs[tx.txid].rate = tx.rate;
this.txs[tx.txid].dirty = true;

View File

@@ -17,7 +17,6 @@ const auditColors = {
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
added: hexToColor('0099ff'),
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
accelerated: hexToColor('8F5FF6'),
};
// convert from this class's update format to TxSprite's update format
@@ -38,9 +37,8 @@ export default class TxView implements TransactionStripped {
vsize: number;
value: number;
feerate: number;
acc?: boolean;
rate?: number;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
context?: 'projected' | 'actual';
scene?: BlockScene;
@@ -65,7 +63,6 @@ export default class TxView implements TransactionStripped {
this.vsize = tx.vsize;
this.value = tx.value;
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
this.acc = tx.acc;
this.rate = tx.rate;
this.status = tx.status;
this.initialised = false;
@@ -202,11 +199,6 @@ export default class TxView implements TransactionStripped {
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Normal mode
if (!this.scene?.highlightingEnabled) {
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
}
return feeLevelColor;
}
// Block audit
@@ -215,7 +207,7 @@ export default class TxView implements TransactionStripped {
return auditColors.censored;
case 'missing':
case 'sigop':
case 'rbf':
case 'fullrbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'fresh':
case 'freshcpfp':
@@ -224,8 +216,6 @@ export default class TxView implements TransactionStripped {
return auditColors.added;
case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
case 'accelerated':
return auditColors.accelerated;
case 'found':
if (this.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
@@ -233,11 +223,7 @@ export default class TxView implements TransactionStripped {
return feeLevelColor;
}
default:
if (this.acc) {
return auditColors.accelerated;
} else {
return feeLevelColor;
}
return feeLevelColor;
}
}
}

View File

@@ -29,8 +29,7 @@
</td>
</tr>
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td>
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td>
@@ -54,8 +53,7 @@
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
</ng-container>
</tr>
</tbody>

View File

@@ -21,7 +21,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
vsize = 1;
feeRate = 0;
effectiveRate;
acceleration;
tooltipPosition: Position = { x: 0, y: 0 };
@@ -54,7 +53,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.vsize = tx.vsize || 1;
this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate;
this.acceleration = tx.acc;
}
}
}

View File

@@ -25,8 +25,7 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -25,8 +25,7 @@
flex-direction: column;
padding: 0px 15px;
width: 100%;
height: calc(100vh - 225px);
min-height: 400px;
height: calc(100vh - 250px);
@media (min-width: 992px) {
height: calc(100vh - 150px);
}

View File

@@ -82,7 +82,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
}),
catchError((err) => {
this.error = err;
this.seoService.logSoft404();
this.openGraphService.fail('block-data-' + this.rawId);
this.openGraphService.fail('block-viz-' + this.rawId);
return of(null);
@@ -139,7 +138,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
(error) => {
this.error = error;
this.isLoadingOverview = false;
this.seoService.logSoft404();
this.openGraphService.fail('block-viz-' + this.rawId);
this.openGraphService.fail('block-data-' + this.rawId);
if (this.blockGraph) {

View File

@@ -206,7 +206,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.error = err;
this.isLoadingBlock = false;
this.isLoadingOverview = false;
this.seoService.logSoft404();
return EMPTY;
})
);
@@ -215,7 +214,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.error = err;
this.isLoadingBlock = false;
this.isLoadingOverview = false;
this.seoService.logSoft404();
return EMPTY;
}),
);
@@ -231,7 +229,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.error = err;
this.isLoadingBlock = false;
this.isLoadingOverview = false;
this.seoService.logSoft404();
return EMPTY;
})
);
@@ -342,17 +339,13 @@ export class BlockComponent implements OnInit, OnDestroy {
const isSelected = {};
const isFresh = {};
const isSigop = {};
const isRbf = {};
const isAccelerated = {};
const isFullRbf = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
isAccelerated[tx.txid] = true;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
@@ -370,10 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
isRbf[txid] = true;
}
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[txid] = true;
isFullRbf[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
@@ -391,17 +381,14 @@ export class BlockComponent implements OnInit, OnDestroy {
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else {
tx.status = 'missing';
}
isMissing[tx.txid] = true;
this.numMissing++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const [index, tx] of transactions.entries()) {
tx.context = 'actual';
@@ -411,16 +398,13 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else if (isFullRbf[tx.txid]) {
tx.status = 'fullrbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;

View File

@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
for (const block of blocks) {
// @ts-ignore: Need to add an extra field for the template
block.extras.pool.logo = `/resources/mining-pools/` +
block.extras.pool.slug + '.svg';
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
}
if (this.widget) {
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
blocks[1][0].extras.pool.slug + '.svg';
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);

View File

@@ -4,56 +4,38 @@
class="difficulty-tooltip"
[style.visibility]="status ? 'visible' : 'hidden'"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + (isMobile ? -60 : 0) + 'px'"
[style.top]="tooltipPosition.y + 'px'"
>
<ng-container *ngIf="!isMobile" [ngSwitch]="status">
<ng-container [ngSwitch]="status">
<ng-container *ngSwitchCase="'mined'">
<ng-container *ngIf="isAhead">
<ng-container *ngTemplateOutlet="expected === 1 ? expectedMinedBlocksSingular : expectedMinedBlocksPlural; context: {$implicit: expected }"></ng-container>
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
</ng-container>
<ng-container *ngIf="!isAhead">
<ng-container *ngTemplateOutlet="mined === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: mined }"></ng-container>
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'remaining'">
<ng-container *ngTemplateOutlet="remaining === 1 ? remainingBlocksSingular : remainingBlocksPlural; context: {$implicit: remaining }"></ng-container>
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'ahead'">
<ng-container *ngTemplateOutlet="ahead === 1 ? aheadBlocksSingular : aheadBlocksPlural; context: {$implicit: ahead }"></ng-container>
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'behind'">
<ng-container *ngTemplateOutlet="behind === 1 ? behindBlocksSingular : behindBlocksPlural; context: {$implicit: behind }"></ng-container>
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'next'">
<span class="next-block" i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">Next Block</span>
</ng-container>
</ng-container>
<ng-container *ngIf="isMobile">
<ng-container *ngIf="isAhead">
<ng-container *ngTemplateOutlet="expected === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: expected }"></ng-container>
</ng-container>
<ng-container *ngIf="!isAhead">
<ng-container *ngTemplateOutlet="mined === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: mined }"></ng-container>
</ng-container>
<br>
<ng-container *ngTemplateOutlet="remaining === 1 ? remainingBlocksSingular : remainingBlocksPlural; context: {$implicit: remaining }"></ng-container>
<br>
<ng-container *ngIf="ahead > 0">
<ng-container *ngTemplateOutlet="ahead === 1 ? aheadBlocksSingular : aheadBlocksPlural; context: {$implicit: ahead }"></ng-container>
</ng-container>
<ng-container *ngIf="behind > 0">
<ng-container *ngTemplateOutlet="behind === 1 ? behindBlocksSingular : behindBlocksPlural; context: {$implicit: behind }"></ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #expectedMinedBlocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
<ng-template #expectedMinedBlocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
<ng-template #minedBlocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
<ng-template #minedBlocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
<ng-template #remainingBlocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
<ng-template #remainingBlocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
<ng-template #aheadBlocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
<ng-template #aheadBlocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
<ng-template #behindBlocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
<ng-template #behindBlocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, ViewChild, Input, OnChanges, HostListener } from '@angular/core';
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
interface EpochProgress {
base: string;
@@ -35,15 +35,12 @@ export class DifficultyTooltipComponent implements OnChanges {
remaining: number;
isAhead: boolean;
isBehind: boolean;
isMobile: boolean;
tooltipPosition = { x: 0, y: 0 };
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {
this.onResize();
}
constructor() {}
ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
@@ -66,9 +63,4 @@ export class DifficultyTooltipComponent implements OnChanges {
this.isBehind = this.behind > 0;
}
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -4,7 +4,7 @@
<div class="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="epoch-progress">
<svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" />
@@ -22,7 +22,7 @@
class="rect {{rect.status}}"
[class.hover]="hoverSection && rect.status === hoverSection.status"
(pointerover)="onHover($event, rect);"
(pointerout)="onBlur();"
(pointerout)="onBlur($event);"
>
<animate
*ngIf="rect.status === 'next'"

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, ElementRef, ViewChild, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
interface EpochProgress {
@@ -44,8 +44,6 @@ export class DifficultyComponent implements OnInit {
@Input() showProgress = true;
@Input() showHalving = false;
@Input() showTitle = true;
@ViewChild('epochSvg') epochSvgElement: ElementRef<SVGElement>;
isLoadingWebSocket$: Observable<boolean>;
difficultyEpoch$: Observable<EpochProgress>;
@@ -63,7 +61,6 @@ export class DifficultyComponent implements OnInit {
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
@Inject(LOCALE_ID) private locale: string,
) { }
@@ -192,27 +189,16 @@ export class DifficultyComponent implements OnInit {
return shapes;
}
@HostListener('pointerdown', ['$event'])
onPointerDown(event): void {
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
this.onPointerMove(event);
event.preventDefault();
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event): void {
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
this.cd.markForCheck();
}
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
}
onHover(_, rect): void {
onHover(event, rect): void {
this.hoverSection = rect;
}
onBlur(): void {
onBlur(event): void {
this.hoverSection = null;
}
}

View File

@@ -64,7 +64,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
return;
}
const samples = [];
const txs = this.transactions.filter(tx => !tx.acc).map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
const sampleInterval = maxBlockVSize / this.numSamples;
let cumVSize = 0;
@@ -74,14 +74,14 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
this.labelInterval = this.numSamples / this.numLabels;
while (nextSample <= maxBlockVSize) {
if (txIndex >= txs.length) {
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0.000001]);
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]);
nextSample += sampleInterval;
sampleIndex++;
continue;
}
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate || 0.000001]);
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]);
nextSample += sampleInterval;
sampleIndex++;
}
@@ -118,9 +118,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
},
},
yAxis: {
type: 'log',
min: 1,
max: this.data.reduce((min, val) => Math.max(min, val[1]), 1),
type: 'value',
// name: 'Effective Fee Rate s/vb',
// nameLocation: 'middle',
splitLine: {
@@ -131,16 +129,12 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
}
},
axisLabel: {
show: true,
formatter: (value: number): string => {
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
return `${newVal}${selectedPowerOfTen.unit}`;
},
},
axisTick: {
show: true,
}
},
series: [{

View File

@@ -1,5 +1,5 @@
<div [formGroup]="fiatForm" class="text-small text-center">
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 150px;" (change)="changeFiat()">
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 200px;" (change)="changeFiat()">
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
</select>
</div>

Some files were not shown because too many files have changed in this diff Show More