Merge branch 'master' into nymkappa/tx-overflow

This commit is contained in:
nymkappa 2023-08-05 10:10:01 +09:00
commit 869a879676
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
81 changed files with 959 additions and 305 deletions

View File

@ -32,7 +32,8 @@
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"DISK_CACHE_BLOCK_INTERVAL": 6, "DISK_CACHE_BLOCK_INTERVAL": 6,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000, "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true "ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@ -115,10 +116,6 @@
"USERNAME": "", "USERNAME": "",
"PASSWORD": "" "PASSWORD": ""
}, },
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
},
"EXTERNAL_DATA_SERVER": { "EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1", "MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1", "MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
@ -137,5 +134,9 @@
"trusted", "trusted",
"servers" "servers"
] ]
},
"MEMPOOL_SERVICES": {
"API": "https://mempool.space/api",
"ACCELERATIONS": false
} }
} }

14
backend/npm_package.sh Executable file
View File

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

View File

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

View File

@ -22,10 +22,10 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json", "tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
"build": "npm run build-rust && npm run tsc && npm run create-resources", "build": "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", "create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
"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": "./npm_package.sh",
"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)", "package-rm-build-deps": "./npm_package_rm_build_deps.sh",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=16384 dist/index.js", "start-production": "node --max-old-space-size=16384 dist/index.js",
"reindex-updated-pools": "npm run start-production --update-pools", "reindex-updated-pools": "npm run start-production --update-pools",
@ -33,8 +33,7 @@
"test": "./node_modules/.bin/jest --coverage", "test": "./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts", "lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
"build-rust": "cd rust-gbt && npm install"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.21.3",
@ -45,7 +44,7 @@
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"express": "~4.18.2", "express": "~4.18.2",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.5.2", "mysql2": "~3.6.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"redis": "^4.6.6", "redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",

View File

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

View File

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

View File

@ -5,7 +5,7 @@ use tracing::{info, trace};
use crate::{ use crate::{
audit_transaction::{partial_cmp_uid_score, AuditTransaction}, audit_transaction::{partial_cmp_uid_score, AuditTransaction},
u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState}, u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
GbtResult, ThreadTransactionsMap, GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration,
}; };
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000; const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
@ -53,7 +53,13 @@ impl Ord for TxPriority {
// TODO: Make gbt smaller to fix these lints. // TODO: Make gbt smaller to fix these lints.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult { 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);
}
let mempool_len = mempool.len(); let mempool_len = mempool.len();
let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1); let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
audit_pool.resize(max_uid + 1, None); audit_pool.resize(max_uid + 1, None);
@ -63,7 +69,8 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
info!("Initializing working structs"); info!("Initializing working structs");
for (uid, tx) in &mut *mempool { for (uid, tx) in &mut *mempool {
let audit_tx = AuditTransaction::from_thread_transaction(tx); let acceleration = indexed_accelerations.get(*uid as usize);
let audit_tx = AuditTransaction::from_thread_transaction(tx, acceleration.copied());
// Safety: audit_pool and mempool_stack must always contain the same transactions // Safety: audit_pool and mempool_stack must always contain the same transactions
audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx)); audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
mempool_stack.push(*uid); mempool_stack.push(*uid);

View File

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

View File

@ -0,0 +1,8 @@
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__", "USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"AUDIT": true, "AUDIT": true,
"ADVANCED_GBT_AUDIT": true, "ADVANCED_GBT_AUDIT": true,
"ADVANCED_GBT_MEMPOOL": true, "ADVANCED_GBT_MEMPOOL": true,
@ -33,7 +33,8 @@
"MAX_BLOCKS_BULK_QUERY": 999, "MAX_BLOCKS_BULK_QUERY": 999,
"DISK_CACHE_BLOCK_INTERVAL": 999, "DISK_CACHE_BLOCK_INTERVAL": 999,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000, "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true "ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 1
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
@ -92,10 +93,6 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__", "USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__" "PASSWORD": "__SOCKS5PROXY_PASSWORD__"
}, },
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": { "EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__", "MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__", "MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@ -129,6 +126,10 @@
"AUDIT_START_HEIGHT": 774000, "AUDIT_START_HEIGHT": 774000,
"SERVERS": [] "SERVERS": []
}, },
"MEMPOOL_SERVICES": {
"API": "",
"ACCELERATIONS": false
},
"REDIS": { "REDIS": {
"ENABLED": false, "ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock" "UNIX_SOCKET_PATH": "/tmp/redis.sock"

View File

@ -47,6 +47,7 @@ describe('Mempool Backend Config', () => {
DISK_CACHE_BLOCK_INTERVAL: 6, DISK_CACHE_BLOCK_INTERVAL: 6,
MAX_PUSH_TX_SIZE_WEIGHT: 400000, MAX_PUSH_TX_SIZE_WEIGHT: 400000,
ALLOW_UNREACHABLE: true, ALLOW_UNREACHABLE: true,
PRICE_UPDATES_PER_HOUR: 1,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
@ -101,11 +102,6 @@ describe('Mempool Backend Config', () => {
PASSWORD: '' 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({ expect(config.EXTERNAL_DATA_SERVER).toStrictEqual({
MEMPOOL_API: 'https://mempool.space/api/v1', MEMPOOL_API: 'https://mempool.space/api/v1',
MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1', MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@ -129,6 +125,11 @@ describe('Mempool Backend Config', () => {
SERVERS: [] SERVERS: []
}); });
expect(config.MEMPOOL_SERVICES).toStrictEqual({
API: "",
ACCELERATIONS: false,
});
expect(config.REDIS).toStrictEqual({ expect(config.REDIS).toStrictEqual({
ENABLED: false, ENABLED: false,
UNIX_SOCKET_PATH: '' UNIX_SOCKET_PATH: ''
@ -163,10 +164,10 @@ describe('Mempool Backend Config', () => {
expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY); expect(config.SOCKS5PROXY).toStrictEqual(fixture.SOCKS5PROXY);
expect(config.PRICE_DATA_SERVER).toStrictEqual(fixture.PRICE_DATA_SERVER);
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER); expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
expect(config.REDIS).toStrictEqual(fixture.REDIS); expect(config.REDIS).toStrictEqual(fixture.REDIS);
}); });
}); });

View File

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

View File

@ -6,16 +6,17 @@ 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 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 { class Audit {
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 }; return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
const added: string[] = []; // present in mined block, not in template const added: string[] = []; // present in mined block, not in template
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
const accelerated: string[] = []; // prioritized by the mempool accelerator
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
@ -28,6 +29,9 @@ class Audit {
const now = Math.round((Date.now() / 1000)); const now = Math.round((Date.now() / 1000));
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = tx; inBlock[tx.txid] = tx;
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
accelerated.push(tx.txid);
}
} }
// coinbase is always expected // coinbase is always expected
if (transactions[0]) { if (transactions[0]) {
@ -149,6 +153,7 @@ class Audit {
fresh, fresh,
sigop: [], sigop: [],
fullrbf: rbf, fullrbf: rbf,
accelerated,
score, score,
similarity, similarity,
}; };

View File

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

View File

@ -60,8 +60,13 @@ class BitcoinApi implements AbstractBitcoinApi {
}); });
} }
$getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> { $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return Promise.resolve([]); 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.');
} }
async $getTransactionHex(txId: string): Promise<string> { async $getTransactionHex(txId: string): Promise<string> {

View File

@ -214,6 +214,7 @@ class BitcoinRoutes {
effectiveFeePerVsize: tx.effectiveFeePerVsize || null, effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
sigops: tx.sigops, sigops: tx.sigops,
adjustedVsize: tx.adjustedVsize, adjustedVsize: tx.adjustedVsize,
acceleration: tx.acceleration
}); });
return; return;
} }

View File

@ -61,6 +61,25 @@ class ElectrsApi implements AbstractBitcoinApi {
}); });
} }
$postWrapper<T>(url, body, responseType = 'json', params: any = undefined): Promise<T> {
return axiosConnection.post<T>(url, body, { ...this.activeAxiosConfig, responseType: responseType, params })
.then((response) => response.data)
.catch((e) => {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
// Retry immediately
return axiosConnection.post<T>(url, body, 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;
}
});
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids'); return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
} }
@ -69,7 +88,11 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId); return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
} }
async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> { async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> {
return this.$postWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
} }

View File

@ -111,6 +111,7 @@ export class Common {
fee: tx.fee || 0, fee: tx.fee || 0,
vsize: tx.weight / 4, vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
acc: tx.acceleration || undefined,
rate: tx.effectiveFeePerVsize, rate: tx.effectiveFeePerVsize,
}; };
} }
@ -460,7 +461,7 @@ export class Common {
}; };
} }
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats { static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): 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); 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; let weightCount = 0;

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 64; private static currentVersion = 65;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -548,6 +548,11 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL'); await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
await this.updateToSchemaVersion(64); 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

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

View File

@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import accelerationApi, { Acceleration } from './services/acceleration';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
class Mempool { class Mempool {
@ -19,9 +20,11 @@ class Mempool {
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private txPerSecondArray: number[] = []; private txPerSecondArray: number[] = [];
private txPerSecond: number = 0; private txPerSecond: number = 0;
@ -66,12 +69,12 @@ class Mempool {
} }
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
this.mempoolChangedCallback = fn; this.mempoolChangedCallback = fn;
} }
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
this.$asyncMempoolChangedCallback = fn; this.$asyncMempoolChangedCallback = fn;
} }
@ -107,10 +110,10 @@ class Mempool {
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`); logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
} }
if (this.mempoolChangedCallback) { if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []); this.mempoolChangedCallback(this.mempoolCache, [], [], []);
} }
if (this.$asyncMempoolChangedCallback) { if (this.$asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []); await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
} }
this.addToSpendMap(Object.values(this.mempoolCache)); this.addToSpendMap(Object.values(this.mempoolCache));
} }
@ -123,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100); loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) { while (!done) {
try { try {
const result = await bitcoinApi.$getMempoolTransactions(last_txid); const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
if (result) { if (result) {
for (const tx of result) { for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@ -231,31 +234,37 @@ class Mempool {
} }
if (!loaded) { if (!loaded) {
for (const txid of transactions) { const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
if (!this.mempoolCache[txid]) { const sliceLength = 10000;
try { for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false); const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
this.updateTimerProgress(timer, 'fetched new transaction'); const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);
this.mempoolCache[txid] = transaction; logger.debug(`fetched ${txs.length} transactions`);
if (this.inSync) { this.updateTimerProgress(timer, 'fetched new transactions');
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) { for (const transaction of txs) {
await redisCache.$addTransaction(transaction); this.mempoolCache[transaction.txid] = transaction;
} if (this.inSync) {
} catch (e: any) { this.txPerSecondArray.push(new Date().getTime());
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) { this.vBytesPerSecondArray.push({
this.missingTxCount++; unixTime: new Date().getTime(),
} vSize: transaction.vsize,
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); });
} }
hasChange = true;
newTransactions.push(transaction);
if (config.REDIS.ENABLED) {
await redisCache.$addTransaction(transaction);
}
}
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 > Math.max(pollRate * 2, 5_000)) {
@ -321,14 +330,19 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); 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); this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.updateTimerProgress(timer, 'running async mempool callback'); this.updateTimerProgress(timer, 'running async mempool callback');
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions); await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta);
this.updateTimerProgress(timer, 'completed async mempool callback'); this.updateTimerProgress(timer, 'completed async mempool callback');
} }
@ -352,6 +366,70 @@ class Mempool {
this.clearTimer(timer); 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() { private startTimer() {
const state: any = { const state: any = {
start: Date.now(), start: Date.now(),

View File

@ -107,6 +107,7 @@ class Mining {
slug: poolInfo.slug, slug: poolInfo.slug,
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null, avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
avgFeeDelta: poolInfo.avgFeeDelta, avgFeeDelta: poolInfo.avgFeeDelta,
poolUniqueId: poolInfo.poolUniqueId
}; };
poolsStats.push(poolStat); poolsStats.push(poolStat);
}); });

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,30 @@
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

@ -4,6 +4,7 @@ import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger'; import logger from '../logger';
import config from '../config';
class TransactionUtils { class TransactionUtils {
constructor() { } constructor() { }
@ -71,6 +72,24 @@ class TransactionUtils {
return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended; 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 { public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore // @ts-ignore
if (transaction.vsize) { if (transaction.vsize) {

View File

@ -21,6 +21,8 @@ import Audit from './audit';
import { deepClone } from '../utils/clone'; import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository'; import { ApiPrice } from '../repositories/PricesRepository';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@ -172,9 +174,15 @@ class WebsocketHandler {
} }
const tx = memPool.getMempool()[trackTxid]; const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) { 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({ response['txPosition'] = JSON.stringify({
txid: trackTxid, txid: trackTxid,
position: tx.position, position
}); });
} }
} else { } else {
@ -390,7 +398,7 @@ class WebsocketHandler {
} }
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> { newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
@ -399,9 +407,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) { if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions); await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
} else { } else {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true); await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} }
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool, true);
@ -647,7 +655,10 @@ class WebsocketHandler {
if (mempoolTx && mempoolTx.position) { if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({ response['txPosition'] = JSON.stringify({
txid: trackTxid, txid: trackTxid,
position: mempoolTx.position, position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
}); });
} }
} }
@ -695,6 +706,7 @@ class WebsocketHandler {
if (config.MEMPOOL.AUDIT && memPool.isInSync()) { if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
let projectedBlocks; let projectedBlocks;
let auditMempool = _memPool; 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 // 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 // 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; const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
@ -702,19 +714,27 @@ class WebsocketHandler {
auditMempool = deepClone(_memPool); auditMempool = deepClone(_memPool);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
if (config.MEMPOOL.RUST_GBT) { if (config.MEMPOOL.RUST_GBT) {
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool); projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
} else { } else {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false); projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
} }
} else { } else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false); projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
} }
} else { } else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); 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();
}
} }
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
const matchRate = Math.round(score * 100 * 100) / 100; const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
@ -743,6 +763,7 @@ class WebsocketHandler {
freshTxs: fresh, freshTxs: fresh,
sigopTxs: sigop, sigopTxs: sigop,
fullrbfTxs: fullrbf, fullrbfTxs: fullrbf,
acceleratedTxs: accelerated,
matchRate: matchRate, matchRate: matchRate,
expectedFees: totalFees, expectedFees: totalFees,
expectedWeight: totalWeight, expectedWeight: totalWeight,
@ -770,9 +791,9 @@ class WebsocketHandler {
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
if (config.MEMPOOL.RUST_GBT) { if (config.MEMPOOL.RUST_GBT) {
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions); await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions, true);
} else { } else {
await mempoolBlocks.$makeBlockTemplates(_memPool, true); await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
} }
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool, true); mempoolBlocks.updateMempoolBlocks(_memPool, true);
@ -836,7 +857,10 @@ class WebsocketHandler {
if (mempoolTx && mempoolTx.position) { if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({ response['txPosition'] = JSON.stringify({
txid: trackTxid, txid: trackTxid,
position: mempoolTx.position, position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
}); });
} }
} }

View File

@ -38,6 +38,7 @@ interface IConfig {
DISK_CACHE_BLOCK_INTERVAL: number; DISK_CACHE_BLOCK_INTERVAL: number;
MAX_PUSH_TX_SIZE_WEIGHT: number; MAX_PUSH_TX_SIZE_WEIGHT: number;
ALLOW_UNREACHABLE: boolean; ALLOW_UNREACHABLE: boolean;
PRICE_UPDATES_PER_HOUR: number;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@ -115,10 +116,6 @@ interface IConfig {
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
}; };
PRICE_DATA_SERVER: {
TOR_URL: string;
CLEARNET_URL: string;
};
EXTERNAL_DATA_SERVER: { EXTERNAL_DATA_SERVER: {
MEMPOOL_API: string; MEMPOOL_API: string;
MEMPOOL_ONION: string; MEMPOOL_ONION: string;
@ -139,6 +136,10 @@ interface IConfig {
AUDIT_START_HEIGHT: number; AUDIT_START_HEIGHT: number;
SERVERS: string[]; SERVERS: string[];
}, },
MEMPOOL_SERVICES: {
API: string;
ACCELERATIONS: boolean;
},
REDIS: { REDIS: {
ENABLED: boolean; ENABLED: boolean;
UNIX_SOCKET_PATH: string; UNIX_SOCKET_PATH: string;
@ -181,6 +182,7 @@ const defaults: IConfig = {
'DISK_CACHE_BLOCK_INTERVAL': 6, 'DISK_CACHE_BLOCK_INTERVAL': 6,
'MAX_PUSH_TX_SIZE_WEIGHT': 400000, 'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
'ALLOW_UNREACHABLE': true, 'ALLOW_UNREACHABLE': true,
'PRICE_UPDATES_PER_HOUR': 1,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
@ -258,10 +260,6 @@ const defaults: IConfig = {
'USERNAME': '', 'USERNAME': '',
'PASSWORD': '' 'PASSWORD': ''
}, },
'PRICE_DATA_SERVER': {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
'EXTERNAL_DATA_SERVER': { 'EXTERNAL_DATA_SERVER': {
'MEMPOOL_API': 'https://mempool.space/api/v1', 'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1', 'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
@ -282,6 +280,10 @@ const defaults: IConfig = {
'AUDIT_START_HEIGHT': 774000, 'AUDIT_START_HEIGHT': 774000,
'SERVERS': [], 'SERVERS': [],
}, },
'MEMPOOL_SERVICES': {
'API': '',
'ACCELERATIONS': false,
},
'REDIS': { 'REDIS': {
'ENABLED': false, 'ENABLED': false,
'UNIX_SOCKET_PATH': '', 'UNIX_SOCKET_PATH': '',
@ -302,10 +304,10 @@ class Config implements IConfig {
LND: IConfig['LND']; LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING']; CLIGHTNING: IConfig['CLIGHTNING'];
SOCKS5PROXY: IConfig['SOCKS5PROXY']; SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND']; MAXMIND: IConfig['MAXMIND'];
REPLICATION: IConfig['REPLICATION']; REPLICATION: IConfig['REPLICATION'];
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
constructor() { constructor() {
@ -323,10 +325,10 @@ class Config implements IConfig {
this.LND = configs.LND; this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING; this.CLIGHTNING = configs.CLIGHTNING;
this.SOCKS5PROXY = configs.SOCKS5PROXY; this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND; this.MAXMIND = configs.MAXMIND;
this.REPLICATION = configs.REPLICATION; this.REPLICATION = configs.REPLICATION;
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
} }

View File

@ -30,6 +30,7 @@ import generalLightningRoutes from './api/explorer/general.routes';
import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
import networkSyncService from './tasks/lightning/network-sync.service'; import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes'; import statisticsRoutes from './api/statistics/statistics.routes';
import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes'; import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes'; import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes'; import liquidRoutes from './api/liquid/liquid.routes';
@ -193,6 +194,7 @@ class Server {
await memPool.$updateMempool(newMempool, pollRate); await memPool.$updateMempool(newMempool, pollRate);
} }
indexer.$run(); indexer.$run();
priceUpdater.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
const elapsed = Date.now() - start; const elapsed = Date.now() - start;
@ -261,6 +263,7 @@ class Server {
setUpHttpApiRoutes(): void { setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app); bitcoinRoutes.initRoutes(this.app);
pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app); statisticsRoutes.initRoutes(this.app);
} }

View File

@ -105,6 +105,12 @@ class Indexer {
return; 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 // Do not attempt to index anything unless Bitcoin Core is fully synced
const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) { if (blockchainInfo.blocks !== blockchainInfo.headers) {
@ -119,8 +125,6 @@ class Indexer {
await this.checkAvailableCoreIndexes(); await this.checkAvailableCoreIndexes();
try { try {
await priceUpdater.$run();
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration

View File

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

View File

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

View File

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

View File

@ -40,7 +40,8 @@ class PoolsRepository {
pools.link AS link, pools.link AS link,
slug, slug,
AVG(blocks_audits.match_rate) AS avgMatchRate, 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 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
FROM blocks FROM blocks
JOIN pools on pools.id = pool_id JOIN pools on pools.id = pool_id
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height

View File

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

View File

@ -113,7 +113,8 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6 "DISK_CACHE_BLOCK_INTERVAL": 6,
"PRICE_UPDATES_PER_HOUR": 1
}, },
``` ```
@ -146,6 +147,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
MEMPOOL_PRICE_UPDATES_PER_HOUR: ""
... ...
``` ```
@ -363,25 +365,6 @@ Corresponding `docker-compose.yml` overrides:
<br/> <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`: `mempool-config.json`:
```json ```json
"LIGHTNING": { "LIGHTNING": {

View File

@ -33,7 +33,8 @@
"MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__, "MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__,
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__" "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
@ -111,10 +112,6 @@
"USERNAME": "__SOCKS5PROXY_USERNAME__", "USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__" "PASSWORD": "__SOCKS5PROXY_PASSWORD__"
}, },
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": { "EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__", "MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__", "MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
@ -135,6 +132,10 @@
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__, "AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
"SERVERS": __REPLICATION_SERVERS__ "SERVERS": __REPLICATION_SERVERS__
}, },
"MEMPOOL_SERVICES": {
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__" "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"

View File

@ -35,7 +35,7 @@ __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6} __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_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true} __MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
__MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@ -94,10 +94,6 @@ __SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""} __SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
__SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""} __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
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1} __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} __EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
@ -137,6 +133,10 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000} __REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
# MEMPOOL_SERVICES
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=true} __REDIS_ENABLED__=${REDIS_ENABLED:=true}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
@ -177,6 +177,7 @@ sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!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_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_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_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
@ -226,9 +227,6 @@ 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_USERNAME__!${__SOCKS5PROXY_USERNAME__}!g" mempool-config.json
sed -i "s!__SOCKS5PROXY_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!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_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_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 sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
@ -267,6 +265,10 @@ 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_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!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 # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json 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 sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

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

View File

@ -31,6 +31,14 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null"> <track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video> </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"> <div class="enterprise-sponsor" id="enterprise-sponsors">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper"> <div class="wrapper">
@ -191,16 +199,41 @@
</div> </div>
</div> </div>
<div class="community-sponsor" id="community-sponsors"> <ng-container *ngIf="officialMempoolSpace">
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3> <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" 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"> <div class="wrapper">
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors"> <ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<ng-template ngFor let-sponsor [ngForOf]="sponsors"> <a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle"> <img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" /> </a>
</a>
</ng-template>
</ng-container> </ng-container>
</div> </div>
</div> </div>
@ -340,7 +373,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators"> <ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key"> <a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" /> <img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a> </a>
</ng-template> </ng-template>
</div> </div>
@ -354,7 +387,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular"> <ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" /> <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span> <span>{{ contributor.name }}</span>
</a> </a>
</ng-template> </ng-template>
@ -366,7 +399,7 @@
<div class="wrapper"> <div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core"> <ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" /> <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span> <span>{{ contributor.name }}</span>
</a> </a>
</ng-template> </ng-template>

View File

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

View File

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

View File

@ -147,7 +147,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) { if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, change, direction, resetLayout);
this.start(); this.start();

View File

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

View File

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

View File

@ -29,7 +29,8 @@
</td> </td>
</tr> </tr>
<tr *ngIf="effectiveRate && effectiveRate !== feeRate"> <tr *ngIf="effectiveRate && effectiveRate !== feeRate">
<td 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">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> <td>
<app-fee-rate [fee]="effectiveRate"></app-fee-rate> <app-fee-rate [fee]="effectiveRate"></app-fee-rate>
</td> </td>
@ -54,6 +55,7 @@
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</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="'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="'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>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>

View File

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

View File

@ -340,12 +340,16 @@ export class BlockComponent implements OnInit, OnDestroy {
const isFresh = {}; const isFresh = {};
const isSigop = {}; const isSigop = {};
const isRbf = {}; const isRbf = {};
const isAccelerated = {};
this.numMissing = 0; this.numMissing = 0;
this.numUnexpected = 0; this.numUnexpected = 0;
if (blockAudit?.template) { if (blockAudit?.template) {
for (const tx of blockAudit.template) { for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true; inTemplate[tx.txid] = true;
if (tx.acc) {
isAccelerated[tx.txid] = true;
}
} }
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = true; inBlock[tx.txid] = true;
@ -365,6 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const txid of blockAudit.fullrbfTxs || []) { for (const txid of blockAudit.fullrbfTxs || []) {
isRbf[txid] = true; isRbf[txid] = true;
} }
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[txid] = true;
}
// set transaction statuses // set transaction statuses
for (const tx of blockAudit.template) { for (const tx of blockAudit.template) {
tx.context = 'projected'; tx.context = 'projected';
@ -389,6 +396,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isMissing[tx.txid] = true; isMissing[tx.txid] = true;
this.numMissing++; this.numMissing++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const [index, tx] of transactions.entries()) { for (const [index, tx] of transactions.entries()) {
tx.context = 'actual'; tx.context = 'actual';
@ -405,6 +415,9 @@ export class BlockComponent implements OnInit, OnDestroy {
isSelected[tx.txid] = true; isSelected[tx.txid] = true;
this.numUnexpected++; this.numUnexpected++;
} }
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
} }
for (const tx of transactions) { for (const tx of transactions) {
inBlock[tx.txid] = true; inBlock[tx.txid] = true;

View File

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

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges, HostListener } from '@angular/core';
interface EpochProgress { interface EpochProgress {
base: string; base: string;
@ -35,12 +35,15 @@ export class DifficultyTooltipComponent implements OnChanges {
remaining: number; remaining: number;
isAhead: boolean; isAhead: boolean;
isBehind: boolean; isBehind: boolean;
isMobile: boolean;
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>; @ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
constructor() {} constructor() {
this.onResize();
}
ngOnChanges(changes): void { ngOnChanges(changes): void {
if (changes.cursorPosition && changes.cursorPosition.currentValue) { if (changes.cursorPosition && changes.cursorPosition.currentValue) {
@ -63,4 +66,9 @@ export class DifficultyTooltipComponent implements OnChanges {
this.isBehind = this.behind > 0; 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="card-body more-padding">
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty"> <div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
<div class="epoch-progress"> <div class="epoch-progress">
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none"> <svg #epochSvg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
<defs> <defs>
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> <linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#105fb0" /> <stop offset="0%" stop-color="#105fb0" />
@ -22,7 +22,7 @@
class="rect {{rect.status}}" class="rect {{rect.status}}"
[class.hover]="hoverSection && rect.status === hoverSection.status" [class.hover]="hoverSection && rect.status === hoverSection.status"
(pointerover)="onHover($event, rect);" (pointerover)="onHover($event, rect);"
(pointerout)="onBlur($event);" (pointerout)="onBlur();"
> >
<animate <animate
*ngIf="rect.status === 'next'" *ngIf="rect.status === 'next'"

View File

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

View File

@ -64,7 +64,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
return; return;
} }
const samples = []; const samples = [];
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 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 maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4; const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
const sampleInterval = maxBlockVSize / this.numSamples; const sampleInterval = maxBlockVSize / this.numSamples;
let cumVSize = 0; let cumVSize = 0;

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="{ val: network$ | async } as network"> <ng-container *ngIf="{ val: network$ | async } as network">
<header> <header *ngIf="headerVisible">
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain"> <ng-template [ngIf]="subdomain">

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Input } from '@angular/core';
import { Env, StateService } from '../../services/state.service'; import { Env, StateService } from '../../services/state.service';
import { Observable, merge, of } from 'rxjs'; import { Observable, merge, of } from 'rxjs';
import { LanguageService } from '../../services/language.service'; import { LanguageService } from '../../services/language.service';
@ -11,6 +11,9 @@ import { NavigationService } from '../../services/navigation.service';
styleUrls: ['./master-page.component.scss'], styleUrls: ['./master-page.component.scss'],
}) })
export class MasterPageComponent implements OnInit { export class MasterPageComponent implements OnInit {
@Input() headerVisible = true;
@Input() footerVisibleOverride: boolean | null = null;
env: Env; env: Env;
network$: Observable<string>; network$: Observable<string>;
connectionState$: Observable<number>; connectionState$: Observable<number>;
@ -38,10 +41,14 @@ export class MasterPageComponent implements OnInit {
this.subdomain = this.enterpriseService.getSubdomain(); this.subdomain = this.enterpriseService.getSubdomain();
this.navigationService.subnetPaths.subscribe((paths) => { this.navigationService.subnetPaths.subscribe((paths) => {
this.networkPaths = paths; this.networkPaths = paths;
if (paths.mainnet.indexOf('docs') > -1) { if (this.footerVisibleOverride === null) {
this.footerVisible = false; if (paths.mainnet.indexOf('docs') > -1) {
this.footerVisible = false;
} else {
this.footerVisible = true;
}
} else { } else {
this.footerVisible = true; this.footerVisible = this.footerVisibleOverride;
} }
}); });
} }

View File

@ -94,7 +94,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
updateBlock(delta: MempoolBlockDelta): void { updateBlock(delta: MempoolBlockDelta): void {
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight); const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) { if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
this.blockGraph.replace(delta.added, direction); this.blockGraph.replace(delta.added, direction);

View File

@ -49,7 +49,7 @@
</div> </div>
</ng-template> </ng-template>
</div> </div>
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }"></div> <div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
</div> </div>
</ng-container> </ng-container>

View File

@ -170,3 +170,33 @@
border-radius: 2px; border-radius: 2px;
z-index: -1; z-index: -1;
} }
.blink{
width:400px;
height:400px;
border-bottom: 35px solid #FFF;
animation: blink 0.2s infinite;
}
@keyframes blink{
0% {
border-bottom: 35px solid green;
}
50% {
border-bottom: 35px solid yellow;
}
100% {
border-bottom: 35px solid orange;
}
}
@-webkit-keyframes blink{
0% {
border-bottom: 35px solid green;
}
50% {
border-bottom: 35px solid yellow;
}
100% {
border-bottom: 35px solid orange;
}
}

View File

@ -26,6 +26,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() minimal: boolean = false; @Input() minimal: boolean = false;
@Input() blockWidth: number = 125; @Input() blockWidth: number = 125;
@Input() containerWidth: number = null;
@Input() count: number = null; @Input() count: number = null;
@Input() spotlight: number = 0; @Input() spotlight: number = 0;
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`; @Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;

View File

@ -99,14 +99,20 @@
</ng-template> </ng-template>
<ng-template #estimationTmpl> <ng-template #estimationTmpl>
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit"> <ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span> <span class="eta d-flex">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
</span>
</ng-template> </ng-template>
<ng-template #belowBlockLimit> <ng-template #belowBlockLimit>
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault"> <ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time> <app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template> </ng-template>
<ng-template #timeEstimateDefault> <ng-template #timeEstimateDefault>
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time> <span class="d-flex">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
</span>
</ng-template> </ng-template>
</ng-template> </ng-template>
</ng-template> </ng-template>
@ -488,7 +494,8 @@
</td> </td>
</tr> </tr>
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate"> <tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td> <td *ngIf="tx.acceleration" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
<td *ngIf="!tx.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td> <td>
<div class="effective-fee-container"> <div class="effective-fee-container">
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate> <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>

View File

@ -217,3 +217,22 @@
display: block; display: block;
} }
} }
.eta {
display: flex;
justify-content: end;
flex-wrap: wrap;
align-content: center;
@media (min-width: 850px) {
justify-content: space-between;
}
}
.accelerate {
align-self: auto;
margin-top: 3px;
@media (min-width: 850px) {
justify-self: start;
margin-left: 0px;
}
}

View File

@ -97,7 +97,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private router: Router, private router: Router,
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService, private electrsApiService: ElectrsApiService,
private stateService: StateService, public stateService: StateService,
private cacheService: CacheService, private cacheService: CacheService,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private audioService: AudioService, private audioService: AudioService,
@ -183,6 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
} else { } else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
} }
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));

View File

@ -19,6 +19,7 @@ export interface Transaction {
ancestors?: Ancestor[]; ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
acceleration?: number;
deleteAfter?: number; deleteAfter?: number;
_unblinded?: any; _unblinded?: any;
_deduced?: boolean; _deduced?: boolean;

View File

@ -27,6 +27,7 @@ export interface CpfpInfo {
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
sigops?: number; sigops?: number;
adjustedVsize?: number; adjustedVsize?: number;
acceleration?: number;
} }
export interface RbfInfo { export interface RbfInfo {
@ -111,6 +112,7 @@ export interface PoolInfo {
addresses: string; // JSON array addresses: string; // JSON array
emptyBlocks: number; emptyBlocks: number;
slug: string; slug: string;
poolUniqueId: number;
} }
export interface PoolStat { export interface PoolStat {
pool: PoolInfo; pool: PoolInfo;
@ -159,6 +161,7 @@ export interface BlockAudit extends BlockExtended {
freshTxs: string[], freshTxs: string[],
sigopTxs: string[], sigopTxs: string[],
fullrbfTxs: string[], fullrbfTxs: string[],
acceleratedTxs: string[],
matchRate: number, matchRate: number,
expectedFees: number, expectedFees: number,
expectedWeight: number, expectedWeight: number,
@ -175,7 +178,8 @@ export interface TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
rate?: number; // effective fee rate rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; acc?: boolean;
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }
@ -187,6 +191,7 @@ export interface RbfTransaction extends TransactionStripped {
export interface MempoolPosition { export interface MempoolPosition {
block: number, block: number,
vsize: number, vsize: number,
accelerated?: boolean
} }
export interface RewardStats { export interface RewardStats {

View File

@ -70,7 +70,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[], added: TransactionStripped[],
removed: string[], removed: string[],
changed?: { txid: string, rate: number | undefined }[]; changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[];
} }
export interface MempoolInfo { export interface MempoolInfo {
@ -88,8 +88,9 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
acc?: boolean; // is accelerated?
rate?: number; // effective fee rate rate?: number; // effective fee rate
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
} }

View File

@ -34,6 +34,7 @@ import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-node
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component'; import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
import { GroupComponent } from './group/group.component'; import { GroupComponent } from './group/group.component';
import { NodeOwnerComponent } from './node-owner/node-owner.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -66,6 +67,7 @@ import { GroupComponent } from './group/group.component';
NodesRankingsDashboard, NodesRankingsDashboard,
NodeChannels, NodeChannels,
GroupComponent, GroupComponent,
NodeOwnerComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -103,6 +105,7 @@ import { GroupComponent } from './group/group.component';
OldestNodes, OldestNodes,
NodesRankingsDashboard, NodesRankingsDashboard,
NodeChannels, NodeChannels,
NodeOwnerComponent,
], ],
providers: [ providers: [
LightningApiService, LightningApiService,

View File

@ -0,0 +1,17 @@
<div *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE === true">
<div *ngIf="{ value: (nodeOwner$ | async) } as nodeOwner">
<div *ngIf="nodeOwner.value && nodeOwner.value.sns_id">
<a target="_blank" [href]="'https://twitter.com/' + nodeOwner.value.username">
<img class="profile-photo" [src]="'data:' + nodeOwner.value.image_mime + ';base64,' + nodeOwner.value.image">
</a>
</div>
<div *ngIf="nodeOwner.value === false">
<a [href]="'/login/lnnode?type=signup&pubkey=' + publicKey + '&alias=' + alias" class="btn btn-primary btn-sm">Claim</a>
<div>
</div>
</div>

View File

@ -0,0 +1,4 @@
.profile-photo {
width: 31px;
height: 31px;
}

View File

@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-node-owner',
templateUrl: './node-owner.component.html',
styleUrls: ['./node-owner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodeOwnerComponent{
@Input() publicKey: string = '';
@Input() alias: string = '';
@Input() nodeOwner$: Observable<any>;
constructor(
public stateService: StateService
) {
}
}

View File

@ -3,13 +3,17 @@
<ng-container *ngIf="!error"> <ng-container *ngIf="!error">
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5> <h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
<div class="title-container mb-2"> <div class="title-container mb-2">
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1> <div class="d-flex justify-content-between align-items-center">
<span class="tx-link"> <h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn"></app-node-owner> -->
</div>
<span class="tx-link justify-content-between align-items-center">
<span class="node-id"> <span class="node-id">
<app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]"> <app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
<app-clipboard [text]="node.public_key"></app-clipboard> <app-clipboard [text]="node.public_key"></app-clipboard>
</app-truncate> </app-truncate>
</span> </span>
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn-mobile"></app-node-owner> -->
</span> </span>
</div> </div>
</ng-container> </ng-container>

View File

@ -111,3 +111,17 @@ app-fiat {
margin: 0 0.25em; margin: 0 0.25em;
color: slategrey; color: slategrey;
} }
.claim-btn {
max-height: 32px;
@media (min-width: 850px) {
display: none;
}
}
.claim-btn-mobile {
max-height: 32px;
@media (max-width: 850px) {
display: none;
}
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable, of, EMPTY } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap, share } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { LightningApiService } from '../lightning-api.service'; import { LightningApiService } from '../lightning-api.service';
@ -38,6 +38,7 @@ export class NodeComponent implements OnInit {
tlvRecords: CustomRecord[]; tlvRecords: CustomRecord[];
avgChannelDistance$: Observable<number | null>; avgChannelDistance$: Observable<number | null>;
showFeatures = false; showFeatures = false;
nodeOwner$: Observable<any>;
kmToMiles = kmToMiles; kmToMiles = kmToMiles;
constructor( constructor(
@ -45,6 +46,7 @@ export class NodeComponent implements OnInit {
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private seoService: SeoService, private seoService: SeoService,
private cd: ChangeDetectorRef,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@ -147,6 +149,24 @@ export class NodeComponent implements OnInit {
return null; return null;
}) })
) as Observable<number | null>; ) as Observable<number | null>;
this.nodeOwner$ = this.activatedRoute.paramMap
.pipe(
switchMap((params: ParamMap) => {
return this.apiService.getNodeOwner$(params.get('public_key')).pipe(
switchMap((response) => {
if (response.status === 204) {
return of(false);
}
return of(response.body);
}),
catchError(() => {
return of(false);
})
)
}),
share(),
);
} }
toggleShowDetails(): void { toggleShowDetails(): void {

View File

@ -8,6 +8,8 @@ import { WebsocketResponse } from '../interfaces/websocket.interface';
import { Outspend, Transaction } from '../interfaces/electrs.interface'; import { Outspend, Transaction } from '../interfaces/electrs.interface';
import { Conversion } from './price.service'; import { Conversion } from './price.service';
const SERVICES_API_PREFIX = `/api/v1/services`;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -92,15 +94,11 @@ export class ApiService {
return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params }); return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
} }
requestDonation$(amount: number, orderId: string): Observable<any> { getAboutPageProfiles$(): Observable<any[]> {
const params = { return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/about-page');
amount: amount,
orderId: orderId,
};
return this.httpClient.post<any>(this.apiBaseUrl + '/api/v1/donations', params);
} }
getDonation$(): Observable<any[]> { getOgs$(): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations'); return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations');
} }
@ -112,10 +110,6 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/contributors'); return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/contributors');
} }
checkDonation$(orderId: string): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations/check?order_id=' + orderId);
}
getInitData$(): Observable<WebsocketResponse> { getInitData$(): Observable<WebsocketResponse> {
return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data'); return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
} }
@ -323,4 +317,13 @@ export class ApiService {
(timestamp ? `?timestamp=${timestamp}` : '') (timestamp ? `?timestamp=${timestamp}` : '')
); );
} }
/**
* Services
*/
getNodeOwner$(publicKey: string) {
let params = new HttpParams()
.set('node_public_key', publicKey);
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/lightning/claim/current`, { params, observe: 'response' });
}
} }

View File

@ -47,6 +47,7 @@ export interface Env {
TESTNET_BLOCK_AUDIT_START_HEIGHT: number; TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number; SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
HISTORICAL_PRICE: boolean; HISTORICAL_PRICE: boolean;
ACCELERATOR: boolean;
} }
const defaultEnv: Env = { const defaultEnv: Env = {
@ -77,6 +78,7 @@ const defaultEnv: Env = {
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0, 'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, 'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'HISTORICAL_PRICE': true, 'HISTORICAL_PRICE': true,
'ACCELERATOR': false,
}; };
@Injectable({ @Injectable({

View File

@ -28,8 +28,9 @@ export class WebsocketService {
private isTrackingTx = false; private isTrackingTx = false;
private trackingTxId: string; private trackingTxId: string;
private isTrackingMempoolBlock = false; private isTrackingMempoolBlock = false;
private isTrackingRbf = false; private isTrackingRbf: 'all' | 'fullRbf' | false = false;
private isTrackingRbfSummary = false; private isTrackingRbfSummary = false;
private isTrackingAddress: string | false = false;
private trackingMempoolBlock: number; private trackingMempoolBlock: number;
private latestGitCommit = ''; private latestGitCommit = '';
private onlineCheckTimeout: number; private onlineCheckTimeout: number;
@ -110,6 +111,15 @@ export class WebsocketService {
if (this.isTrackingMempoolBlock) { if (this.isTrackingMempoolBlock) {
this.startTrackMempoolBlock(this.trackingMempoolBlock); this.startTrackMempoolBlock(this.trackingMempoolBlock);
} }
if (this.isTrackingRbf) {
this.startTrackRbf(this.isTrackingRbf);
}
if (this.isTrackingRbfSummary) {
this.startTrackRbfSummary();
}
if (this.isTrackingAddress) {
this.startTrackAddress(this.isTrackingAddress);
}
this.stateService.connectionState$.next(2); this.stateService.connectionState$.next(2);
} }
@ -151,10 +161,12 @@ export class WebsocketService {
startTrackAddress(address: string) { startTrackAddress(address: string) {
this.websocketSubject.next({ 'track-address': address }); this.websocketSubject.next({ 'track-address': address });
this.isTrackingAddress = address;
} }
stopTrackingAddress() { stopTrackingAddress() {
this.websocketSubject.next({ 'track-address': 'stop' }); this.websocketSubject.next({ 'track-address': 'stop' });
this.isTrackingAddress = false;
} }
startTrackAsset(asset: string) { startTrackAsset(asset: string) {
@ -178,7 +190,7 @@ export class WebsocketService {
startTrackRbf(mode: 'all' | 'fullRbf') { startTrackRbf(mode: 'all' | 'fullRbf') {
this.websocketSubject.next({ 'track-rbf': mode }); this.websocketSubject.next({ 'track-rbf': mode });
this.isTrackingRbf = true; this.isTrackingRbf = mode;
} }
stopTrackRbf() { stopTrackRbf() {

View File

@ -1,7 +1,7 @@
<footer> <footer>
<div class="container-fluid"> <div class="container-fluid">
<div class="row main"> <div class="row main">
<div class="offset-lg-1 col-lg-4 col align-self-center branding"> <div class="offset-lg-1 col-lg-4 col align-self-center branding mt-2">
<div class="main-logo"> <div class="main-logo">
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
@ -16,10 +16,12 @@
<div class="selector"> <div class="selector">
<app-rate-unit-selector></app-rate-unit-selector> <app-rate-unit-selector></app-rate-unit-selector>
</div> </div>
<ng-template #temporaryHidden> <div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta">
<a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a> <a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]">
<p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p> <span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
</ng-template> <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
</a>
</div>
</div> </div>
<div class="col-lg-6 col-md-10 offset-md-1 links outer"> <div class="col-lg-6 col-md-10 offset-md-1 links outer">
<div class="row"> <div class="row">

View File

@ -22,7 +22,7 @@ footer .row.main .branding {
} }
footer .row.main .branding > p { footer .row.main .branding > p {
margin-bottom: 45px; margin-bottom: 25px;
} }
footer .row.main .branding .btn { footer .row.main .branding .btn {
@ -35,11 +35,7 @@ footer .row.main .branding button.account {
} }
footer .row.main .branding .cta { footer .row.main .branding .cta {
margin: 20px auto 25px auto; margin: 25px auto 25px auto;
}
footer .row.main .branding .cta-secondary {
} }
footer .row.main .links.outer { footer .row.main .links.outer {

View File

@ -1,10 +1,13 @@
import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
import { Observable, merge, of, Subject } from 'rxjs'; import { ActivatedRoute } from '@angular/router';
import { Observable, merge, of, Subject, Subscription } from 'rxjs';
import { tap, takeUntil } from 'rxjs/operators'; import { tap, takeUntil } from 'rxjs/operators';
import { Env, StateService } from '../../../services/state.service'; import { Env, StateService } from '../../../services/state.service';
import { IBackendInfo } from '../../../interfaces/websocket.interface'; import { IBackendInfo } from '../../../interfaces/websocket.interface';
import { LanguageService } from '../../../services/language.service'; import { LanguageService } from '../../../services/language.service';
import { NavigationService } from '../../../services/navigation.service'; import { NavigationService } from '../../../services/navigation.service';
import { StorageService } from '../../../services/storage.service';
import { WebsocketService } from '../../../services/websocket.service';
@Component({ @Component({
selector: 'app-global-footer', selector: 'app-global-footer',
@ -23,12 +26,19 @@ export class GlobalFooterComponent implements OnInit {
network$: Observable<string>; network$: Observable<string>;
networkPaths: { [network: string]: string }; networkPaths: { [network: string]: string };
currentNetwork = ''; currentNetwork = '';
loggedIn = false;
username = null;
urlSubscription: Subscription;
constructor( constructor(
public stateService: StateService, public stateService: StateService,
private languageService: LanguageService, private languageService: LanguageService,
private navigationService: NavigationService, private navigationService: NavigationService,
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
private storageService: StorageService,
private route: ActivatedRoute,
private cd: ChangeDetectorRef,
private websocketService: WebsocketService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -46,11 +56,23 @@ export class GlobalFooterComponent implements OnInit {
this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => { this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => {
this.currentNetwork = network; this.currentNetwork = network;
}); });
this.urlSubscription = this.route.url.subscribe((url) => {
this.loggedIn = JSON.parse(this.storageService.getValue('auth')) !== null;
const auth = JSON.parse(this.storageService.getValue('auth'));
if (auth?.user?.username) {
this.username = auth.user.username;
} else {
this.username = null;
}
this.cd.markForCheck();
})
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(true); this.destroy$.next(true);
this.destroy$.complete(); this.destroy$.complete();
this.urlSubscription.unsubscribe();
} }
networkLink(network) { networkLink(network) {

View File

@ -219,6 +219,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AmountShortenerPipe, AmountShortenerPipe,
], ],
exports: [ exports: [
MasterPageComponent,
RouterModule, RouterModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbNavModule, NgbNavModule,

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@ -18,7 +18,8 @@
"USE_SECOND_NODE_FOR_MINFEE": true, "USE_SECOND_NODE_FOR_MINFEE": true,
"DISK_CACHE_BLOCK_INTERVAL": 1, "DISK_CACHE_BLOCK_INTERVAL": 1,
"MAX_PUSH_TX_SIZE_WEIGHT": 4000000, "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
"ALLOW_UNREACHABLE": true "ALLOW_UNREACHABLE": true,
"PRICE_UPDATES_PER_HOUR": 12
}, },
"SYSLOG" : { "SYSLOG" : {
"MIN_PRIORITY": "debug" "MIN_PRIORITY": "debug"