Merge branch 'master' into nymkappa/tx-overflow
This commit is contained in:
		
						commit
						869a879676
					
				| @ -32,7 +32,8 @@ | ||||
|     "CPFP_INDEXING": false, | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 6, | ||||
|     "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, | ||||
|     "ALLOW_UNREACHABLE": true | ||||
|     "ALLOW_UNREACHABLE": true, | ||||
|     "PRICE_UPDATES_PER_HOUR": 1 | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "127.0.0.1", | ||||
| @ -115,10 +116,6 @@ | ||||
|     "USERNAME": "", | ||||
|     "PASSWORD": "" | ||||
|   }, | ||||
|   "PRICE_DATA_SERVER": { | ||||
|     "TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices", | ||||
|     "CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices" | ||||
|   }, | ||||
|   "EXTERNAL_DATA_SERVER": { | ||||
|     "MEMPOOL_API": "https://mempool.space/api/v1", | ||||
|     "MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1", | ||||
| @ -137,5 +134,9 @@ | ||||
|       "trusted", | ||||
|       "servers" | ||||
|     ] | ||||
|   }, | ||||
|   "MEMPOOL_SERVICES": { | ||||
|     "API": "https://mempool.space/api", | ||||
|     "ACCELERATIONS": false | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										14
									
								
								backend/npm_package.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								backend/npm_package.sh
									
									
									
									
									
										Executable 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 | ||||
							
								
								
									
										12
									
								
								backend/npm_package_rm_build_deps.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										12
									
								
								backend/npm_package_rm_build_deps.sh
									
									
									
									
									
										Executable 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 | ||||
							
								
								
									
										16
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -17,9 +17,9 @@ | ||||
|         "crypto-js": "~4.1.1", | ||||
|         "express": "~4.18.2", | ||||
|         "maxmind": "~4.3.11", | ||||
|         "mysql2": "~3.5.2", | ||||
|         "rust-gbt": "file:./rust-gbt", | ||||
|         "mysql2": "~3.6.0", | ||||
|         "redis": "^4.6.6", | ||||
|         "rust-gbt": "file:./rust-gbt", | ||||
|         "socks-proxy-agent": "~7.0.0", | ||||
|         "typescript": "~4.9.3", | ||||
|         "ws": "~8.13.0" | ||||
| @ -6102,9 +6102,9 @@ | ||||
|       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" | ||||
|     }, | ||||
|     "node_modules/mysql2": { | ||||
|       "version": "3.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz", | ||||
|       "integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==", | ||||
|       "version": "3.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", | ||||
|       "integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==", | ||||
|       "dependencies": { | ||||
|         "denque": "^2.1.0", | ||||
|         "generate-function": "^2.3.1", | ||||
| @ -12212,9 +12212,9 @@ | ||||
|       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" | ||||
|     }, | ||||
|     "mysql2": { | ||||
|       "version": "3.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.5.2.tgz", | ||||
|       "integrity": "sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==", | ||||
|       "version": "3.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", | ||||
|       "integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==", | ||||
|       "requires": { | ||||
|         "denque": "^2.1.0", | ||||
|         "generate-function": "^2.3.1", | ||||
|  | ||||
| @ -22,10 +22,10 @@ | ||||
|   "main": "index.ts", | ||||
|   "scripts": { | ||||
|     "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", | ||||
|     "package": "npm run build && rm -rf package && mv dist package && mv node_modules package && mv rust-gbt package && npm run package-rm-build-deps", | ||||
|     "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint @napi-rs ../rust-gbt/target ../rust-gbt/node_modules ../rust-gbt/src)", | ||||
|     "package": "./npm_package.sh", | ||||
|     "package-rm-build-deps": "./npm_package_rm_build_deps.sh", | ||||
|     "start": "node --max-old-space-size=2048 dist/index.js", | ||||
|     "start-production": "node --max-old-space-size=16384 dist/index.js", | ||||
|     "reindex-updated-pools": "npm run start-production --update-pools", | ||||
| @ -33,8 +33,7 @@ | ||||
|     "test": "./node_modules/.bin/jest --coverage", | ||||
|     "lint": "./node_modules/.bin/eslint . --ext .ts", | ||||
|     "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", | ||||
|     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", | ||||
|     "build-rust": "cd rust-gbt && npm install" | ||||
|     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.21.3", | ||||
| @ -45,7 +44,7 @@ | ||||
|     "crypto-js": "~4.1.1", | ||||
|     "express": "~4.18.2", | ||||
|     "maxmind": "~4.3.11", | ||||
|     "mysql2": "~3.5.2", | ||||
|     "mysql2": "~3.6.0", | ||||
|     "rust-gbt": "file:./rust-gbt", | ||||
|     "redis": "^4.6.6", | ||||
|     "socks-proxy-agent": "~7.0.0", | ||||
|  | ||||
							
								
								
									
										8
									
								
								backend/rust-gbt/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								backend/rust-gbt/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -12,6 +12,10 @@ export interface ThreadTransaction { | ||||
|   effectiveFeePerVsize: number | ||||
|   inputs: Array<number> | ||||
| } | ||||
| export interface ThreadAcceleration { | ||||
|   uid: number | ||||
|   delta: number | ||||
| } | ||||
| export class GbtGenerator { | ||||
|   constructor() | ||||
|   /** | ||||
| @ -19,13 +23,13 @@ export class GbtGenerator { | ||||
|    * | ||||
|    * 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 | ||||
|    * | ||||
|    * 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. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| use crate::{ | ||||
|     u32_hasher_types::{u32hashset_new, U32HasherState}, | ||||
|     ThreadTransaction, | ||||
|     ThreadTransaction, thread_acceleration::ThreadAcceleration, | ||||
| }; | ||||
| use std::{ | ||||
|     cmp::Ordering, | ||||
| @ -88,44 +88,49 @@ impl Ord for AuditTransaction { | ||||
| } | ||||
| 
 | ||||
| #[inline] | ||||
| fn calc_fee_rate(fee: f64, vsize: f64) -> f64 { | ||||
|     fee / (if vsize == 0.0 { 1.0 } else { vsize }) | ||||
| fn calc_fee_rate(fee: u64, vsize: f64) -> f64 { | ||||
|     (fee as f64) / (if vsize == 0.0 { 1.0 } else { vsize }) | ||||
| } | ||||
| 
 | ||||
| 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
 | ||||
|         let is_adjusted = tx.weight < (tx.sigops * 20); | ||||
|         let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5); | ||||
|         let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20); | ||||
|         let effective_fee_per_vsize = if is_adjusted { | ||||
|             calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0) | ||||
|         let effective_fee_per_vsize = if is_adjusted || fee_delta > 0.0 { | ||||
|             calc_fee_rate(fee, f64::from(sigop_adjusted_weight) / 4.0) | ||||
|         } else { | ||||
|             tx.effective_fee_per_vsize | ||||
|         }; | ||||
|         Self { | ||||
|             uid: tx.uid, | ||||
|             order: tx.order, | ||||
|             fee: tx.fee as u64, | ||||
|             fee, | ||||
|             weight: tx.weight, | ||||
|             sigop_adjusted_weight, | ||||
|             sigop_adjusted_vsize, | ||||
|             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, | ||||
|             dependency_rate: f64::INFINITY, | ||||
|             inputs: tx.inputs.clone(), | ||||
|             relatives_set_flag: false, | ||||
|             ancestors: u32hashset_new(), | ||||
|             children: u32hashset_new(), | ||||
|             ancestor_fee: tx.fee as u64, | ||||
|             ancestor_fee: fee, | ||||
|             ancestor_sigop_adjusted_weight: sigop_adjusted_weight, | ||||
|             ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize, | ||||
|             ancestor_sigops: tx.sigops, | ||||
|             score: 0.0, | ||||
|             used: false, | ||||
|             modified: false, | ||||
|             dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize, | ||||
|             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
 | ||||
|         // the smaller of the two. If either side is NaN, the other side is returned.
 | ||||
|         self.dependency_rate.min(calc_fee_rate( | ||||
|             self.ancestor_fee as f64, | ||||
|             self.ancestor_fee, | ||||
|             f64::from(self.ancestor_sigop_adjusted_weight) / 4.0, | ||||
|         )) | ||||
|     } | ||||
| @ -172,7 +177,7 @@ impl AuditTransaction { | ||||
|     #[inline] | ||||
|     fn calc_new_score(&mut self) { | ||||
|         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), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
| @ -5,7 +5,7 @@ use tracing::{info, trace}; | ||||
| use crate::{ | ||||
|     audit_transaction::{partial_cmp_uid_score, AuditTransaction}, | ||||
|     u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState}, | ||||
|     GbtResult, ThreadTransactionsMap, | ||||
|     GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration, | ||||
| }; | ||||
| 
 | ||||
| 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.
 | ||||
| #[allow(clippy::too_many_lines)] | ||||
| #[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 mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1); | ||||
|     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"); | ||||
|     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
 | ||||
|         audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx)); | ||||
|         mempool_stack.push(*uid); | ||||
|  | ||||
| @ -9,6 +9,7 @@ | ||||
| use napi::bindgen_prelude::Result; | ||||
| use napi_derive::napi; | ||||
| use thread_transaction::ThreadTransaction; | ||||
| use thread_acceleration::ThreadAcceleration; | ||||
| use tracing::{debug, info, trace}; | ||||
| use tracing_log::LogTracer; | ||||
| use tracing_subscriber::{EnvFilter, FmtSubscriber}; | ||||
| @ -19,6 +20,7 @@ use std::sync::{Arc, Mutex}; | ||||
| mod audit_transaction; | ||||
| mod gbt; | ||||
| mod thread_transaction; | ||||
| mod thread_acceleration; | ||||
| mod u32_hasher_types; | ||||
| 
 | ||||
| use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState}; | ||||
| @ -74,10 +76,11 @@ impl GbtGenerator { | ||||
|     ///
 | ||||
|     /// Rejects if the thread panics or if the Mutex is poisoned.
 | ||||
|     #[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); | ||||
|         run_task( | ||||
|             Arc::clone(&self.thread_transactions), | ||||
|             accelerations, | ||||
|             max_uid as usize, | ||||
|             move |map| { | ||||
|                 for tx in mempool { | ||||
| @ -96,11 +99,13 @@ impl GbtGenerator { | ||||
|         &self, | ||||
|         new_txs: Vec<ThreadTransaction>, | ||||
|         remove_txs: Vec<u32>, | ||||
|         accelerations: Vec<ThreadAcceleration>, | ||||
|         max_uid: u32, | ||||
|     ) -> Result<GbtResult> { | ||||
|         trace!("update: Current State {:#?}", self.thread_transactions); | ||||
|         run_task( | ||||
|             Arc::clone(&self.thread_transactions), | ||||
|             accelerations, | ||||
|             max_uid as usize, | ||||
|             move |map| { | ||||
|                 for tx in new_txs { | ||||
| @ -141,6 +146,7 @@ pub struct GbtResult { | ||||
| /// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
 | ||||
| async fn run_task<F>( | ||||
|     thread_transactions: Arc<Mutex<ThreadTransactionsMap>>, | ||||
|     accelerations: Vec<ThreadAcceleration>, | ||||
|     max_uid: usize, | ||||
|     callback: F, | ||||
| ) -> Result<GbtResult> | ||||
| @ -159,7 +165,7 @@ where | ||||
|         callback(&mut map); | ||||
| 
 | ||||
|         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()); | ||||
| 
 | ||||
|         debug!( | ||||
|  | ||||
							
								
								
									
										8
									
								
								backend/rust-gbt/src/thread_acceleration.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								backend/rust-gbt/src/thread_acceleration.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| use napi_derive::napi; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| #[napi(object)] | ||||
| pub struct ThreadAcceleration { | ||||
|     pub uid: u32, | ||||
|     pub delta: f64, // fee delta
 | ||||
| } | ||||
| @ -23,8 +23,8 @@ | ||||
|     "USER_AGENT": "__MEMPOOL_USER_AGENT__", | ||||
|     "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", | ||||
|     "INDEXING_BLOCKS_AMOUNT": 14, | ||||
|     "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__POOLS_JSON_URL__", | ||||
|     "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", | ||||
|     "AUDIT": true, | ||||
|     "ADVANCED_GBT_AUDIT": true, | ||||
|     "ADVANCED_GBT_MEMPOOL": true, | ||||
| @ -33,7 +33,8 @@ | ||||
|     "MAX_BLOCKS_BULK_QUERY": 999, | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 999, | ||||
|     "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, | ||||
|     "ALLOW_UNREACHABLE": true | ||||
|     "ALLOW_UNREACHABLE": true, | ||||
|     "PRICE_UPDATES_PER_HOUR": 1 | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "__CORE_RPC_HOST__", | ||||
| @ -92,10 +93,6 @@ | ||||
|     "USERNAME": "__SOCKS5PROXY_USERNAME__", | ||||
|     "PASSWORD": "__SOCKS5PROXY_PASSWORD__" | ||||
|   }, | ||||
|   "PRICE_DATA_SERVER": { | ||||
|     "TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__", | ||||
|     "CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__" | ||||
|   }, | ||||
|   "EXTERNAL_DATA_SERVER": { | ||||
|     "MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__", | ||||
|     "MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__", | ||||
| @ -129,6 +126,10 @@ | ||||
|     "AUDIT_START_HEIGHT": 774000, | ||||
|     "SERVERS": [] | ||||
|   }, | ||||
|   "MEMPOOL_SERVICES": { | ||||
|     "API": "", | ||||
|     "ACCELERATIONS": false | ||||
|   }, | ||||
|   "REDIS": { | ||||
|     "ENABLED": false, | ||||
|     "UNIX_SOCKET_PATH": "/tmp/redis.sock" | ||||
|  | ||||
| @ -47,6 +47,7 @@ describe('Mempool Backend Config', () => { | ||||
|         DISK_CACHE_BLOCK_INTERVAL: 6, | ||||
|         MAX_PUSH_TX_SIZE_WEIGHT: 400000, | ||||
|         ALLOW_UNREACHABLE: true, | ||||
|         PRICE_UPDATES_PER_HOUR: 1, | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); | ||||
| @ -101,11 +102,6 @@ describe('Mempool Backend Config', () => { | ||||
|         PASSWORD: '' | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.PRICE_DATA_SERVER).toStrictEqual({ | ||||
|         TOR_URL: 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices', | ||||
|         CLEARNET_URL: 'https://price.bisq.wiz.biz/getAllMarketPrices' | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.EXTERNAL_DATA_SERVER).toStrictEqual({ | ||||
|         MEMPOOL_API: 'https://mempool.space/api/v1', | ||||
|         MEMPOOL_ONION: 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1', | ||||
| @ -129,6 +125,11 @@ describe('Mempool Backend Config', () => { | ||||
|         SERVERS: [] | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.MEMPOOL_SERVICES).toStrictEqual({ | ||||
|         API: "", | ||||
|         ACCELERATIONS: false, | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.REDIS).toStrictEqual({ | ||||
|         ENABLED: false, | ||||
|         UNIX_SOCKET_PATH: '' | ||||
| @ -163,10 +164,10 @@ describe('Mempool Backend Config', () => { | ||||
| 
 | ||||
|       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.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES); | ||||
| 
 | ||||
|       expect(config.REDIS).toStrictEqual(fixture.REDIS); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import fs from 'fs'; | ||||
| import { GbtGenerator, ThreadTransaction } from '../../../rust-gbt'; | ||||
| import { GbtGenerator, ThreadTransaction } from 'rust-gbt'; | ||||
| import path from 'path'; | ||||
| 
 | ||||
| const baseline = require('./test-data/target-template.json'); | ||||
| @ -15,7 +15,7 @@ describe('Rust GBT', () => { | ||||
|   test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => { | ||||
|     const rustGbt = new GbtGenerator(); | ||||
|     const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer); | ||||
|     const result = await rustGbt.make(mempool, maxUid); | ||||
|     const result = await rustGbt.make(mempool, [], maxUid); | ||||
| 
 | ||||
|     const blocks: [string, number][][] = result.blocks.map(block => { | ||||
|       return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]); | ||||
|  | ||||
| @ -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
 | ||||
| 
 | ||||
| class Audit { | ||||
|   auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) | ||||
|    : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } { | ||||
|   auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) | ||||
|    : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { | ||||
|     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 added: string[] = []; // present in mined block, not in template
 | ||||
|     const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
 | ||||
|     const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
 | ||||
|     const accelerated: string[] = []; // prioritized by the mempool accelerator
 | ||||
|     const isCensored = {}; // missing, without excuse
 | ||||
|     const isDisplaced = {}; | ||||
|     let displacedWeight = 0; | ||||
| @ -28,6 +29,9 @@ class Audit { | ||||
|     const now = Math.round((Date.now() / 1000)); | ||||
|     for (const tx of transactions) { | ||||
|       inBlock[tx.txid] = tx; | ||||
|       if (mempool[tx.txid] && mempool[tx.txid].acceleration) { | ||||
|         accelerated.push(tx.txid); | ||||
|       } | ||||
|     } | ||||
|     // coinbase is always expected
 | ||||
|     if (transactions[0]) { | ||||
| @ -149,6 +153,7 @@ class Audit { | ||||
|       fresh, | ||||
|       sigop: [], | ||||
|       fullrbf: rbf, | ||||
|       accelerated, | ||||
|       score, | ||||
|       similarity, | ||||
|     }; | ||||
|  | ||||
| @ -3,7 +3,8 @@ import { IEsploraApi } from './esplora-api.interface'; | ||||
| export interface AbstractBitcoinApi { | ||||
|   $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>; | ||||
|   $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>; | ||||
|   $getMempoolTransactions(lastTxid: string); | ||||
|   $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; | ||||
|   $getAllMempoolTransactions(lastTxid: string); | ||||
|   $getTransactionHex(txId: string): Promise<string>; | ||||
|   $getBlockHeightTip(): Promise<number>; | ||||
|   $getBlockHashTip(): Promise<string>; | ||||
|  | ||||
| @ -60,8 +60,13 @@ class BitcoinApi implements AbstractBitcoinApi { | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   $getMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> { | ||||
|     return Promise.resolve([]); | ||||
|   $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> { | ||||
|     throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); | ||||
|   } | ||||
| 
 | ||||
|   $getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> { | ||||
|     throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.'); | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   async $getTransactionHex(txId: string): Promise<string> { | ||||
|  | ||||
| @ -214,6 +214,7 @@ class BitcoinRoutes { | ||||
|           effectiveFeePerVsize: tx.effectiveFeePerVsize || null, | ||||
|           sigops: tx.sigops, | ||||
|           adjustedVsize: tx.adjustedVsize, | ||||
|           acceleration: tx.acceleration | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
| @ -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'][]> { | ||||
|     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); | ||||
|   } | ||||
| 
 | ||||
|   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 : '')); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -111,6 +111,7 @@ export class Common { | ||||
|       fee: tx.fee || 0, | ||||
|       vsize: tx.weight / 4, | ||||
|       value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), | ||||
|       acc: tx.acceleration || undefined, | ||||
|       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); | ||||
| 
 | ||||
|     let weightCount = 0; | ||||
|  | ||||
| @ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; | ||||
| import { RowDataPacket } from 'mysql2'; | ||||
| 
 | ||||
| class DatabaseMigration { | ||||
|   private static currentVersion = 64; | ||||
|   private static currentVersion = 65; | ||||
|   private queryTimeout = 3600_000; | ||||
|   private statisticsAddedIndexed = false; | ||||
|   private uniqueLogs: string[] = []; | ||||
| @ -548,6 +548,11 @@ class DatabaseMigration { | ||||
|       await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL'); | ||||
|       await this.updateToSchemaVersion(64); | ||||
|     } | ||||
| 
 | ||||
|     if (databaseSchemaVersion < 65 && isBitcoin === true) { | ||||
|       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"'); | ||||
|       await this.updateToSchemaVersion(65); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -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 { 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 config from '../config'; | ||||
| import { Worker } from 'worker_threads'; | ||||
| import path from 'path'; | ||||
| import mempool from './mempool'; | ||||
| 
 | ||||
| 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++) { | ||||
|       let added: TransactionStripped[] = []; | ||||
|       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]) { | ||||
|         added = mempoolBlocks[i].transactions; | ||||
|       } else if (!mempoolBlocks[i] && prevBlocks[i]) { | ||||
| @ -192,8 +193,8 @@ class MempoolBlocks { | ||||
|         mempoolBlocks[i].transactions.forEach(tx => { | ||||
|           if (!prevIds[tx.txid]) { | ||||
|             added.push(tx); | ||||
|           } else if (tx.rate !== prevIds[tx.txid].rate) { | ||||
|             changed.push({ txid: tx.txid, rate: tx.rate }); | ||||
|           } else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) { | ||||
|             changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc }); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
| @ -206,14 +207,19 @@ class MempoolBlocks { | ||||
|     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(); | ||||
| 
 | ||||
|     // reset mempool short ids
 | ||||
|     if (saveResults) { | ||||
|       this.resetUids(); | ||||
|     for (const tx of Object.values(newMempool)) { | ||||
|       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
 | ||||
|     // 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) { | ||||
|         const stripped = { | ||||
|           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), | ||||
|           sigops: entry.sigops, | ||||
|           feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize, | ||||
| @ -262,7 +268,7 @@ class MempoolBlocks { | ||||
|       // clean up thread error listener
 | ||||
|       this.txSelectionWorker?.removeListener('error', threadErrorListener); | ||||
| 
 | ||||
|       const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), 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`); | ||||
| 
 | ||||
| @ -273,25 +279,29 @@ class 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) { | ||||
|       // need to reset the worker
 | ||||
|       await this.$makeBlockTemplates(newMempool, saveResults); | ||||
|       await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
|     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
 | ||||
|     // 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 { | ||||
|         uid: entry.uid || 0, | ||||
|         fee: entry.fee, | ||||
|         fee: entry.fee + (useAccelerations ? (accelerations[entry.txid]?.feeDelta || 0) : 0), | ||||
|         weight: (entry.adjustedVsize * 4), | ||||
|         sigops: entry.sigops, | ||||
|         feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize, | ||||
| @ -318,7 +328,7 @@ class MempoolBlocks { | ||||
|       // clean up thread error listener
 | ||||
|       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`); | ||||
|     } catch (e) { | ||||
|       logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); | ||||
| @ -330,7 +340,7 @@ class MempoolBlocks { | ||||
|     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(); | ||||
| 
 | ||||
|     // 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[]; | ||||
|     } | ||||
| 
 | ||||
|     const accelerations = useAccelerations ? mempool.getAccelerations() : {}; | ||||
|     const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]); | ||||
|     const convertedAccelerations = acceleratedList.map(acc => { | ||||
|       return { | ||||
|         uid: this.getUid(newMempool[acc.txid]), | ||||
|         delta: acc.feeDelta, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     // run the block construction algorithm in a separate thread, and wait for a result
 | ||||
|     const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(); | ||||
|     try { | ||||
|       const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids( | ||||
|         await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], this.nextUid), | ||||
|         await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid), | ||||
|       ); | ||||
|       if (saveResults) { | ||||
|         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`); | ||||
|       return processed; | ||||
|     } catch (e) { | ||||
| @ -367,20 +386,20 @@ class MempoolBlocks { | ||||
|     return this.mempoolBlocks; | ||||
|   } | ||||
| 
 | ||||
|   public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> { | ||||
|     return this.$rustMakeBlockTemplates(newMempool, false); | ||||
|   public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> { | ||||
|     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
 | ||||
|     // as a sanity check, we should also explicitly prevent uint32 uid overflow
 | ||||
|     if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) { | ||||
|       this.resetRustGbt(); | ||||
|     } | ||||
| 
 | ||||
|     if (!this.rustInitialized) { | ||||
|       // need to reset the worker
 | ||||
|       await this.$rustMakeBlockTemplates(newMempool, true); | ||||
|       return; | ||||
|       return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool); | ||||
|     } | ||||
| 
 | ||||
|     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 accelerations = useAccelerations ? mempool.getAccelerations() : {}; | ||||
|     const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]); | ||||
|     const convertedAccelerations = acceleratedList.map(acc => { | ||||
|       return { | ||||
|         uid: this.getUid(newMempool[acc.txid]), | ||||
|         delta: acc.feeDelta, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     // run the block construction algorithm in a separate thread, and wait for a result
 | ||||
|     try { | ||||
|       const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids( | ||||
|         await this.rustGbtGenerator.update( | ||||
|           added as RustThreadTransaction[], | ||||
|           removedUids, | ||||
|           convertedAccelerations as RustThreadAcceleration[], | ||||
|           this.nextUid, | ||||
|         ), | ||||
|       ); | ||||
| @ -407,17 +436,19 @@ class MempoolBlocks { | ||||
|       if (mempoolSize !== resultMempoolSize) { | ||||
|         throw new Error('GBT returned wrong number of transactions, cache is probably out of sync'); | ||||
|       } 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; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); | ||||
|       this.resetRustGbt(); | ||||
|       return this.mempoolBlocks; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], 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) { | ||||
|       if (txid in mempool) { | ||||
|         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; | ||||
|     // update this thread's mempool with the results
 | ||||
|     let mempoolTx: MempoolTransactionExtended; | ||||
| @ -496,6 +529,17 @@ class MempoolBlocks { | ||||
|             mempoolTx.cpfpChecked = true; | ||||
|           } | ||||
| 
 | ||||
|           const acceleration = accelerations[txid]; | ||||
|           if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { | ||||
|             mempoolTx.acceleration = true; | ||||
|             for (const ancestor of mempoolTx.ancestors || []) { | ||||
|               mempool[ancestor.txid].acceleration = true; | ||||
|               isAccelerated[ancestor.txid] = true; | ||||
|             } | ||||
|           } else { | ||||
|             delete mempoolTx.acceleration; | ||||
|           } | ||||
| 
 | ||||
|           // online calculation of stack-of-blocks fee stats
 | ||||
|           if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) { | ||||
|             feeStatsCalculator.processNext(mempoolTx); | ||||
| @ -532,7 +576,7 @@ class MempoolBlocks { | ||||
| 
 | ||||
|   private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions { | ||||
|     if (!feeStats) { | ||||
|       feeStats = Common.calcEffectiveFeeStatistics(transactions); | ||||
|       feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration)); | ||||
|     } | ||||
|     return { | ||||
|       blockSize: totalSize, | ||||
|  | ||||
| @ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators'; | ||||
| import bitcoinClient from './bitcoin/bitcoin-client'; | ||||
| import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; | ||||
| import rbfCache from './rbf-cache'; | ||||
| import accelerationApi, { Acceleration } from './services/acceleration'; | ||||
| import redisCache from './redis-cache'; | ||||
| 
 | ||||
| class Mempool { | ||||
| @ -19,9 +20,11 @@ class Mempool { | ||||
|   private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, | ||||
|                                                     maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; | ||||
|   private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], | ||||
|     deletedTransactions: MempoolTransactionExtended[]) => void) | undefined; | ||||
|     deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; | ||||
|   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 txPerSecond: number = 0; | ||||
| @ -66,12 +69,12 @@ class Mempool { | ||||
|   } | ||||
| 
 | ||||
|   public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, | ||||
|     newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void { | ||||
|     newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { | ||||
|     this.mempoolChangedCallback = fn; | ||||
|   } | ||||
| 
 | ||||
|   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; | ||||
|   } | ||||
| 
 | ||||
| @ -107,10 +110,10 @@ class Mempool { | ||||
|       logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`); | ||||
|     } | ||||
|     if (this.mempoolChangedCallback) { | ||||
|       this.mempoolChangedCallback(this.mempoolCache, [], []); | ||||
|       this.mempoolChangedCallback(this.mempoolCache, [], [], []); | ||||
|     } | ||||
|     if (this.$asyncMempoolChangedCallback) { | ||||
|       await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []); | ||||
|       await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []); | ||||
|     } | ||||
|     this.addToSpendMap(Object.values(this.mempoolCache)); | ||||
|   } | ||||
| @ -123,7 +126,7 @@ class Mempool { | ||||
|     loadingIndicators.setProgress('mempool', count / expectedCount * 100); | ||||
|     while (!done) { | ||||
|       try { | ||||
|         const result = await bitcoinApi.$getMempoolTransactions(last_txid); | ||||
|         const result = await bitcoinApi.$getAllMempoolTransactions(last_txid); | ||||
|         if (result) { | ||||
|           for (const tx of result) { | ||||
|             const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); | ||||
| @ -231,12 +234,16 @@ class Mempool { | ||||
|     } | ||||
| 
 | ||||
|     if (!loaded) { | ||||
|       for (const txid of transactions) { | ||||
|         if (!this.mempoolCache[txid]) { | ||||
|           try { | ||||
|             const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false); | ||||
|             this.updateTimerProgress(timer, 'fetched new transaction'); | ||||
|             this.mempoolCache[txid] = transaction; | ||||
|       const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]); | ||||
|       const sliceLength = 10000; | ||||
|       for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) { | ||||
|         const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength); | ||||
|         const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false); | ||||
|         logger.debug(`fetched ${txs.length} transactions`); | ||||
|         this.updateTimerProgress(timer, 'fetched new transactions'); | ||||
| 
 | ||||
|         for (const transaction of txs) { | ||||
|           this.mempoolCache[transaction.txid] = transaction; | ||||
|           if (this.inSync) { | ||||
|             this.txPerSecondArray.push(new Date().getTime()); | ||||
|             this.vBytesPerSecondArray.push({ | ||||
| @ -250,12 +257,14 @@ class Mempool { | ||||
|           if (config.REDIS.ENABLED) { | ||||
|             await redisCache.$addTransaction(transaction); | ||||
|           } | ||||
|           } catch (e: any) { | ||||
|             if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) { | ||||
|               this.missingTxCount++; | ||||
|         } | ||||
|             logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); | ||||
| 
 | ||||
|         if (txs.length < slice.length) { | ||||
|           const missing = slice.length - txs.length; | ||||
|           if (config.MEMPOOL.BACKEND === 'esplora') { | ||||
|             this.missingTxCount += missing; | ||||
|           } | ||||
|           logger.debug(`Error finding ${missing} transactions in the mempool: `); | ||||
|         } | ||||
| 
 | ||||
|         if (Date.now() - intervalTimer > Math.max(pollRate * 2, 5_000)) { | ||||
| @ -321,14 +330,19 @@ class Mempool { | ||||
|     const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); | ||||
|     this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); | ||||
| 
 | ||||
|     const accelerationDelta = await this.$updateAccelerations(); | ||||
|     if (accelerationDelta.length) { | ||||
|       hasChange = true; | ||||
|     } | ||||
| 
 | ||||
|     this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize); | ||||
| 
 | ||||
|     if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { | ||||
|       this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); | ||||
|       this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); | ||||
|     } | ||||
|     if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) { | ||||
|       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'); | ||||
|     } | ||||
| 
 | ||||
| @ -352,6 +366,70 @@ class Mempool { | ||||
|     this.clearTimer(timer); | ||||
|   } | ||||
| 
 | ||||
|   public getAccelerations(): { [txid: string]: Acceleration } { | ||||
|     return this.accelerations; | ||||
|   } | ||||
| 
 | ||||
|   public async $updateAccelerations(): Promise<string[]> { | ||||
|     if (!config.MEMPOOL_SERVICES.ACCELERATIONS) { | ||||
|       return []; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       const newAccelerations = await accelerationApi.$fetchAccelerations(); | ||||
| 
 | ||||
|       const changed: string[] = []; | ||||
| 
 | ||||
|       const newAccelerationMap: { [txid: string]: Acceleration } = {}; | ||||
|       for (const acceleration of newAccelerations) { | ||||
|         newAccelerationMap[acceleration.txid] = acceleration; | ||||
|         if (this.accelerations[acceleration.txid] == null) { | ||||
|           // new acceleration
 | ||||
|           changed.push(acceleration.txid); | ||||
|         } else { | ||||
|           if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) { | ||||
|             // feeDelta changed
 | ||||
|             changed.push(acceleration.txid); | ||||
|           } else if (this.accelerations[acceleration.txid].pools?.length) { | ||||
|             let poolsChanged = false; | ||||
|             const pools = new Set(); | ||||
|             this.accelerations[acceleration.txid].pools.forEach(pool => { | ||||
|               pools.add(pool); | ||||
|             }); | ||||
|             acceleration.pools.forEach(pool => { | ||||
|               if (!pools.has(pool)) { | ||||
|                 poolsChanged = true; | ||||
|               } else { | ||||
|                 pools.delete(pool); | ||||
|               } | ||||
|             }); | ||||
|             if (pools.size > 0) { | ||||
|               poolsChanged = true; | ||||
|             } | ||||
|             if (poolsChanged) { | ||||
|               // pools changed
 | ||||
|               changed.push(acceleration.txid); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       for (const oldTxid of Object.keys(this.accelerations)) { | ||||
|         if (!newAccelerationMap[oldTxid]) { | ||||
|           // removed
 | ||||
|           changed.push(oldTxid); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.accelerations = newAccelerationMap; | ||||
| 
 | ||||
|       return changed; | ||||
|     } catch (e: any) { | ||||
|       logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private startTimer() { | ||||
|     const state: any = { | ||||
|       start: Date.now(), | ||||
|  | ||||
| @ -107,6 +107,7 @@ class Mining { | ||||
|         slug: poolInfo.slug, | ||||
|         avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null, | ||||
|         avgFeeDelta: poolInfo.avgFeeDelta, | ||||
|         poolUniqueId: poolInfo.poolUniqueId | ||||
|       }; | ||||
|       poolsStats.push(poolStat); | ||||
|     }); | ||||
|  | ||||
							
								
								
									
										19
									
								
								backend/src/api/prices/prices.routes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								backend/src/api/prices/prices.routes.ts
									
									
									
									
									
										Normal 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(); | ||||
							
								
								
									
										30
									
								
								backend/src/api/services/acceleration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								backend/src/api/services/acceleration.ts
									
									
									
									
									
										Normal 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(); | ||||
| @ -4,6 +4,7 @@ import { Common } from './common'; | ||||
| import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | ||||
| import * as bitcoinjs from 'bitcoinjs-lib'; | ||||
| import logger from '../logger'; | ||||
| import config from '../config'; | ||||
| 
 | ||||
| class TransactionUtils { | ||||
|   constructor() { } | ||||
| @ -71,6 +72,24 @@ class TransactionUtils { | ||||
|     return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended; | ||||
|   } | ||||
| 
 | ||||
|   public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> { | ||||
|     if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|       const results = await Promise.allSettled(txids.map(txid => this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, true))); | ||||
|       return (results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<MempoolTransactionExtended>[]).map(r => r.value); | ||||
|     } else { | ||||
|       const transactions = await bitcoinApi.$getMempoolTransactions(txids); | ||||
|       return transactions.map(transaction => { | ||||
|         if (Common.isLiquid()) { | ||||
|           if (!isFinite(Number(transaction.fee))) { | ||||
|             transaction.fee = Object.values(transaction.fee || {}).reduce((total, output) => total + output, 0); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         return this.extendMempoolTransaction(transaction); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { | ||||
|     // @ts-ignore
 | ||||
|     if (transaction.vsize) { | ||||
|  | ||||
| @ -21,6 +21,8 @@ import Audit from './audit'; | ||||
| import { deepClone } from '../utils/clone'; | ||||
| import priceUpdater from '../tasks/price-updater'; | ||||
| import { ApiPrice } from '../repositories/PricesRepository'; | ||||
| import accelerationApi from './services/acceleration'; | ||||
| import mempool from './mempool'; | ||||
| 
 | ||||
| // valid 'want' subscriptions
 | ||||
| const wantable = [ | ||||
| @ -172,9 +174,15 @@ class WebsocketHandler { | ||||
|               } | ||||
|               const tx = memPool.getMempool()[trackTxid]; | ||||
|               if (tx && tx.position) { | ||||
|                 const position: { block: number, vsize: number, accelerated?: boolean } = { | ||||
|                   ...tx.position | ||||
|                 }; | ||||
|                 if (tx.acceleration) { | ||||
|                   position.accelerated = tx.acceleration; | ||||
|                 } | ||||
|                 response['txPosition'] = JSON.stringify({ | ||||
|                   txid: trackTxid, | ||||
|                   position: tx.position, | ||||
|                   position | ||||
|                 }); | ||||
|               } | ||||
|             } else { | ||||
| @ -390,7 +398,7 @@ class WebsocketHandler { | ||||
|   } | ||||
| 
 | ||||
|   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) { | ||||
|       throw new Error('WebSocket.Server is not set'); | ||||
|     } | ||||
| @ -399,9 +407,9 @@ class WebsocketHandler { | ||||
| 
 | ||||
|     if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { | ||||
|       if (config.MEMPOOL.RUST_GBT) { | ||||
|         await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions); | ||||
|         await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS); | ||||
|       } else { | ||||
|         await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true); | ||||
|         await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS); | ||||
|       } | ||||
|     } else { | ||||
|       mempoolBlocks.updateMempoolBlocks(newMempool, true); | ||||
| @ -647,7 +655,10 @@ class WebsocketHandler { | ||||
|         if (mempoolTx && mempoolTx.position) { | ||||
|           response['txPosition'] = JSON.stringify({ | ||||
|             txid: trackTxid, | ||||
|             position: mempoolTx.position, | ||||
|             position: { | ||||
|               ...mempoolTx.position, | ||||
|               accelerated: mempoolTx.acceleration || undefined, | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
| @ -695,6 +706,7 @@ class WebsocketHandler { | ||||
|     if (config.MEMPOOL.AUDIT && memPool.isInSync()) { | ||||
|       let projectedBlocks; | ||||
|       let auditMempool = _memPool; | ||||
|       const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); | ||||
|       // template calculation functions have mempool side effects, so calculate audits using
 | ||||
|       // a cloned copy of the mempool if we're running a different algorithm for mempool updates
 | ||||
|       const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL; | ||||
| @ -702,19 +714,27 @@ class WebsocketHandler { | ||||
|         auditMempool = deepClone(_memPool); | ||||
|         if (config.MEMPOOL.ADVANCED_GBT_AUDIT) { | ||||
|           if (config.MEMPOOL.RUST_GBT) { | ||||
|             projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool); | ||||
|             projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id); | ||||
|           } else { | ||||
|             projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false); | ||||
|             projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id); | ||||
|           } | ||||
|         } else { | ||||
|           projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false); | ||||
|         } | ||||
|       } else { | ||||
|         if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) { | ||||
|           if (config.MEMPOOL.RUST_GBT) { | ||||
|             projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id); | ||||
|           } else { | ||||
|             projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id); | ||||
|           } | ||||
|         } else { | ||||
|           projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       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 stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; | ||||
| @ -743,6 +763,7 @@ class WebsocketHandler { | ||||
|           freshTxs: fresh, | ||||
|           sigopTxs: sigop, | ||||
|           fullrbfTxs: fullrbf, | ||||
|           acceleratedTxs: accelerated, | ||||
|           matchRate: matchRate, | ||||
|           expectedFees: totalFees, | ||||
|           expectedWeight: totalWeight, | ||||
| @ -770,9 +791,9 @@ class WebsocketHandler { | ||||
| 
 | ||||
|     if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { | ||||
|       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 { | ||||
|         await mempoolBlocks.$makeBlockTemplates(_memPool, true); | ||||
|         await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS); | ||||
|       } | ||||
|     } else { | ||||
|       mempoolBlocks.updateMempoolBlocks(_memPool, true); | ||||
| @ -836,7 +857,10 @@ class WebsocketHandler { | ||||
|           if (mempoolTx && mempoolTx.position) { | ||||
|             response['txPosition'] = JSON.stringify({ | ||||
|               txid: trackTxid, | ||||
|               position: mempoolTx.position, | ||||
|               position: { | ||||
|                 ...mempoolTx.position, | ||||
|                 accelerated: mempoolTx.acceleration || undefined, | ||||
|               } | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|  | ||||
| @ -38,6 +38,7 @@ interface IConfig { | ||||
|     DISK_CACHE_BLOCK_INTERVAL: number; | ||||
|     MAX_PUSH_TX_SIZE_WEIGHT: number; | ||||
|     ALLOW_UNREACHABLE: boolean; | ||||
|     PRICE_UPDATES_PER_HOUR: number; | ||||
|   }; | ||||
|   ESPLORA: { | ||||
|     REST_API_URL: string; | ||||
| @ -115,10 +116,6 @@ interface IConfig { | ||||
|     USERNAME: string; | ||||
|     PASSWORD: string; | ||||
|   }; | ||||
|   PRICE_DATA_SERVER: { | ||||
|     TOR_URL: string; | ||||
|     CLEARNET_URL: string; | ||||
|   }; | ||||
|   EXTERNAL_DATA_SERVER: { | ||||
|     MEMPOOL_API: string; | ||||
|     MEMPOOL_ONION: string; | ||||
| @ -139,6 +136,10 @@ interface IConfig { | ||||
|     AUDIT_START_HEIGHT: number; | ||||
|     SERVERS: string[]; | ||||
|   }, | ||||
|   MEMPOOL_SERVICES: { | ||||
|     API: string; | ||||
|     ACCELERATIONS: boolean; | ||||
|   }, | ||||
|   REDIS: { | ||||
|     ENABLED: boolean; | ||||
|     UNIX_SOCKET_PATH: string; | ||||
| @ -181,6 +182,7 @@ const defaults: IConfig = { | ||||
|     'DISK_CACHE_BLOCK_INTERVAL': 6, | ||||
|     'MAX_PUSH_TX_SIZE_WEIGHT': 400000, | ||||
|     'ALLOW_UNREACHABLE': true, | ||||
|     'PRICE_UPDATES_PER_HOUR': 1, | ||||
|   }, | ||||
|   'ESPLORA': { | ||||
|     'REST_API_URL': 'http://127.0.0.1:3000', | ||||
| @ -258,10 +260,6 @@ const defaults: IConfig = { | ||||
|     'USERNAME': '', | ||||
|     'PASSWORD': '' | ||||
|   }, | ||||
|   'PRICE_DATA_SERVER': { | ||||
|     'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices', | ||||
|     'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices' | ||||
|   }, | ||||
|   'EXTERNAL_DATA_SERVER': { | ||||
|     'MEMPOOL_API': 'https://mempool.space/api/v1', | ||||
|     'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1', | ||||
| @ -282,6 +280,10 @@ const defaults: IConfig = { | ||||
|     'AUDIT_START_HEIGHT': 774000, | ||||
|     'SERVERS': [], | ||||
|   }, | ||||
|   'MEMPOOL_SERVICES': { | ||||
|     'API': '', | ||||
|     'ACCELERATIONS': false, | ||||
|   }, | ||||
|   'REDIS': { | ||||
|     'ENABLED': false, | ||||
|     'UNIX_SOCKET_PATH': '', | ||||
| @ -302,10 +304,10 @@ class Config implements IConfig { | ||||
|   LND: IConfig['LND']; | ||||
|   CLIGHTNING: IConfig['CLIGHTNING']; | ||||
|   SOCKS5PROXY: IConfig['SOCKS5PROXY']; | ||||
|   PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; | ||||
|   EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; | ||||
|   MAXMIND: IConfig['MAXMIND']; | ||||
|   REPLICATION: IConfig['REPLICATION']; | ||||
|   MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; | ||||
|   REDIS: IConfig['REDIS']; | ||||
| 
 | ||||
|   constructor() { | ||||
| @ -323,10 +325,10 @@ class Config implements IConfig { | ||||
|     this.LND = configs.LND; | ||||
|     this.CLIGHTNING = configs.CLIGHTNING; | ||||
|     this.SOCKS5PROXY = configs.SOCKS5PROXY; | ||||
|     this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; | ||||
|     this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; | ||||
|     this.MAXMIND = configs.MAXMIND; | ||||
|     this.REPLICATION = configs.REPLICATION; | ||||
|     this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; | ||||
|     this.REDIS = configs.REDIS; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -30,6 +30,7 @@ import generalLightningRoutes from './api/explorer/general.routes'; | ||||
| import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; | ||||
| import networkSyncService from './tasks/lightning/network-sync.service'; | ||||
| import statisticsRoutes from './api/statistics/statistics.routes'; | ||||
| import pricesRoutes from './api/prices/prices.routes'; | ||||
| import miningRoutes from './api/mining/mining-routes'; | ||||
| import bisqRoutes from './api/bisq/bisq.routes'; | ||||
| import liquidRoutes from './api/liquid/liquid.routes'; | ||||
| @ -193,6 +194,7 @@ class Server { | ||||
|         await memPool.$updateMempool(newMempool, pollRate); | ||||
|       } | ||||
|       indexer.$run(); | ||||
|       priceUpdater.$run(); | ||||
| 
 | ||||
|       // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
 | ||||
|       const elapsed = Date.now() - start; | ||||
| @ -261,6 +263,7 @@ class Server { | ||||
|    | ||||
|   setUpHttpApiRoutes(): void { | ||||
|     bitcoinRoutes.initRoutes(this.app); | ||||
|     pricesRoutes.initRoutes(this.app); | ||||
|     if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { | ||||
|       statisticsRoutes.initRoutes(this.app); | ||||
|     } | ||||
|  | ||||
| @ -105,6 +105,12 @@ class Indexer { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       await priceUpdater.$run(); | ||||
|     } catch (e) { | ||||
|       logger.err(`Running priceUpdater failed. Reason: ` + (e instanceof Error ? e.message : e)); | ||||
|     } | ||||
| 
 | ||||
|     // Do not attempt to index anything unless Bitcoin Core is fully synced
 | ||||
|     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); | ||||
|     if (blockchainInfo.blocks !== blockchainInfo.headers) { | ||||
| @ -119,8 +125,6 @@ class Indexer { | ||||
|     await this.checkAvailableCoreIndexes(); | ||||
| 
 | ||||
|     try { | ||||
|       await priceUpdater.$run(); | ||||
| 
 | ||||
|       const chainValid = await blocks.$generateBlockDatabase(); | ||||
|       if (chainValid === false) { | ||||
|         // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
 | ||||
|  | ||||
| @ -20,6 +20,7 @@ export interface PoolInfo { | ||||
|   slug: string; | ||||
|   avgMatchRate: number | null; | ||||
|   avgFeeDelta: number | null; | ||||
|   poolUniqueId: number; | ||||
| } | ||||
| 
 | ||||
| export interface PoolStats extends PoolInfo { | ||||
| @ -36,6 +37,7 @@ export interface BlockAudit { | ||||
|   sigopTxs: string[], | ||||
|   fullrbfTxs: string[], | ||||
|   addedTxs: string[], | ||||
|   acceleratedTxs: string[], | ||||
|   matchRate: number, | ||||
|   expectedFees?: number, | ||||
|   expectedWeight?: number, | ||||
| @ -91,6 +93,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction { | ||||
|     block: number, | ||||
|     vsize: number, | ||||
|   }; | ||||
|   acceleration?: boolean; | ||||
|   uid?: number; | ||||
| } | ||||
| 
 | ||||
| @ -182,6 +185,7 @@ export interface TransactionStripped { | ||||
|   fee: number; | ||||
|   vsize: number; | ||||
|   value: number; | ||||
|   acc?: boolean; | ||||
|   rate?: number; // effective fee rate
 | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -116,6 +116,7 @@ class AuditReplication { | ||||
|       freshTxs: auditSummary.freshTxs || [], | ||||
|       sigopTxs: auditSummary.sigopTxs || [], | ||||
|       fullrbfTxs: auditSummary.fullrbfTxs || [], | ||||
|       acceleratedTxs: auditSummary.acceleratedTxs || [], | ||||
|       matchRate: auditSummary.matchRate, | ||||
|       expectedFees: auditSummary.expectedFees, | ||||
|       expectedWeight: auditSummary.expectedWeight, | ||||
|  | ||||
| @ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces'; | ||||
| class BlocksAuditRepositories { | ||||
|   public async $saveAudit(audit: BlockAudit): Promise<void> { | ||||
|     try { | ||||
|       await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
 | ||||
|         VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | ||||
|           JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); | ||||
|       await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
 | ||||
|         VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 | ||||
|           JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); | ||||
|     } catch (e: any) { | ||||
|       if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | ||||
|         logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`); | ||||
| @ -69,6 +69,7 @@ class BlocksAuditRepositories { | ||||
|         fresh_txs as freshTxs, | ||||
|         sigop_txs as sigopTxs, | ||||
|         fullrbf_txs as fullrbfTxs, | ||||
|         accelerated_txs as acceleratedTxs, | ||||
|         match_rate as matchRate, | ||||
|         expected_fees as expectedFees, | ||||
|         expected_weight as expectedWeight | ||||
| @ -83,6 +84,7 @@ class BlocksAuditRepositories { | ||||
|         rows[0].freshTxs = JSON.parse(rows[0].freshTxs); | ||||
|         rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs); | ||||
|         rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs); | ||||
|         rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs); | ||||
|         rows[0].template = JSON.parse(rows[0].template); | ||||
| 
 | ||||
|         return rows[0]; | ||||
|  | ||||
| @ -40,7 +40,8 @@ class PoolsRepository { | ||||
|           pools.link AS link, | ||||
|           slug, | ||||
|           AVG(blocks_audits.match_rate) AS avgMatchRate, | ||||
|           AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta | ||||
|           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 | ||||
|       JOIN pools on pools.id = pool_id | ||||
|       LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height | ||||
|  | ||||
| @ -25,7 +25,10 @@ export interface PriceHistory { | ||||
| 
 | ||||
| class PriceUpdater { | ||||
|   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 running = false; | ||||
|   private feeds: PriceFeed[] = []; | ||||
| @ -41,6 +44,8 @@ class PriceUpdater { | ||||
|     this.feeds.push(new CoinbaseApi()); | ||||
|     this.feeds.push(new BitfinexApi()); | ||||
|     this.feeds.push(new GeminiApi()); | ||||
| 
 | ||||
|     this.setCyclePosition(); | ||||
|   } | ||||
| 
 | ||||
|   public getLatestPrices(): ApiPrice { | ||||
| @ -100,22 +105,48 @@ class PriceUpdater { | ||||
|     this.running = false; | ||||
|   } | ||||
| 
 | ||||
|   private getMillisecondsSinceBeginningOfHour(): number { | ||||
|     const now = new Date(); | ||||
|     const beginningOfHour = new Date(now); | ||||
|     beginningOfHour.setMinutes(0, 0, 0); | ||||
|     return now.getTime() - beginningOfHour.getTime(); | ||||
|   } | ||||
| 
 | ||||
|   private setCyclePosition(): void { | ||||
|     const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour(); | ||||
|     for (let i = 0; i < config.MEMPOOL.PRICE_UPDATES_PER_HOUR; i++) { | ||||
|       if (this.timeBetweenUpdatesMs * i > millisecondsSinceBeginningOfHour) { | ||||
|         this.cyclePosition = i; | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     this.cyclePosition = config.MEMPOOL.PRICE_UPDATES_PER_HOUR; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Fetch last BTC price from exchanges, average them, and save it in the database once every hour | ||||
|    */ | ||||
|   private async $updatePrice(): Promise<void> { | ||||
|     if (this.lastRun === 0 && config.DATABASE.ENABLED === true) { | ||||
|       this.lastRun = await PricesRepository.$getLatestPriceTime(); | ||||
|     let forceUpdate = false; | ||||
|     if (this.firstRun === true && config.DATABASE.ENABLED === true) { | ||||
|       const lastUpdate = await PricesRepository.$getLatestPriceTime(); | ||||
|       if (new Date().getTime() / 1000 - lastUpdate > this.timeBetweenUpdatesMs / 1000) { | ||||
|         forceUpdate = true; | ||||
|       } | ||||
|       this.firstRun = false; | ||||
|     } | ||||
| 
 | ||||
|     if ((Math.round(new Date().getTime() / 1000) - this.lastRun) < 3600) { | ||||
|       // Refresh only once every hour
 | ||||
|     const millisecondsSinceBeginningOfHour = this.getMillisecondsSinceBeginningOfHour(); | ||||
| 
 | ||||
|     // Reset the cycle on new hour
 | ||||
|     if (this.lastTime > millisecondsSinceBeginningOfHour) { | ||||
|       this.cyclePosition = 0; | ||||
|     } | ||||
|     this.lastTime = millisecondsSinceBeginningOfHour; | ||||
|     if (millisecondsSinceBeginningOfHour < this.timeBetweenUpdatesMs * this.cyclePosition && !forceUpdate && this.cyclePosition !== 0) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const previousRun = this.lastRun; | ||||
|     this.lastRun = new Date().getTime() / 1000; | ||||
| 
 | ||||
|     for (const currency of this.currencies) { | ||||
|       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) { | ||||
|     if (config.DATABASE.ENABLED === true && this.cyclePosition === 0) { | ||||
|       // Save everything in db
 | ||||
|       try { | ||||
|         const p = 60 * 60 * 1000; // milliseconds in an hour
 | ||||
|         const nowRounded = new Date(Math.round(new Date().getTime() / p) * p); // https://stackoverflow.com/a/28037042
 | ||||
|         this.latestPrices.time = nowRounded.getTime() / 1000; | ||||
|         await PricesRepository.$savePrices(nowRounded.getTime() / 1000, this.latestPrices); | ||||
|       } catch (e) { | ||||
|         this.lastRun = previousRun + 5 * 60; | ||||
|         logger.err(`Cannot save latest prices into db. Trying again in 5 minutes. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.latestPrices.time = Math.round(new Date().getTime() / 1000); | ||||
|     logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`); | ||||
| 
 | ||||
|     if (this.ratesChangedCallback) { | ||||
|       this.ratesChangedCallback(this.latestPrices); | ||||
|     } | ||||
| 
 | ||||
|     this.lastRun = new Date().getTime() / 1000; | ||||
|     if (!forceUpdate) { | ||||
|       this.cyclePosition++; | ||||
|     } | ||||
| 
 | ||||
|     if (this.latestPrices.USD === -1) { | ||||
|       this.latestPrices = await PricesRepository.$getLatestConversionRates(); | ||||
|  | ||||
| @ -113,7 +113,8 @@ Below we list all settings from `mempool-config.json` and the corresponding over | ||||
|     "ADVANCED_GBT_MEMPOOL": false, | ||||
|     "CPFP_INDEXING": false, | ||||
|     "MAX_BLOCKS_BULK_QUERY": 0, | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 6 | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 6, | ||||
|     "PRICE_UPDATES_PER_HOUR": 1 | ||||
|   }, | ||||
| ``` | ||||
| 
 | ||||
| @ -146,6 +147,7 @@ Corresponding `docker-compose.yml` overrides: | ||||
|       MEMPOOL_CPFP_INDEXING: "" | ||||
|       MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" | ||||
|       MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" | ||||
|       MEMPOOL_PRICE_UPDATES_PER_HOUR: "" | ||||
|       ... | ||||
| ``` | ||||
| 
 | ||||
| @ -363,25 +365,6 @@ Corresponding `docker-compose.yml` overrides: | ||||
| 
 | ||||
| <br/> | ||||
| 
 | ||||
| `mempool-config.json`: | ||||
| ```json | ||||
|   "PRICE_DATA_SERVER": { | ||||
|     "TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices", | ||||
|     "CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices" | ||||
|   } | ||||
| ``` | ||||
| 
 | ||||
| Corresponding `docker-compose.yml` overrides: | ||||
| ```yaml | ||||
|   api: | ||||
|     environment: | ||||
|       PRICE_DATA_SERVER_TOR_URL: "" | ||||
|       PRICE_DATA_SERVER_CLEARNET_URL: "" | ||||
|       ... | ||||
| ``` | ||||
| 
 | ||||
| <br/> | ||||
| 
 | ||||
| `mempool-config.json`: | ||||
| ```json | ||||
|   "LIGHTNING": { | ||||
|  | ||||
| @ -33,7 +33,8 @@ | ||||
|     "MAX_PUSH_TX_SIZE_WEIGHT": __MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__, | ||||
|     "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, | ||||
|     "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", | ||||
|     "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__" | ||||
|     "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", | ||||
|     "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__ | ||||
|   }, | ||||
|   "CORE_RPC": { | ||||
|     "HOST": "__CORE_RPC_HOST__", | ||||
| @ -111,10 +112,6 @@ | ||||
|     "USERNAME": "__SOCKS5PROXY_USERNAME__", | ||||
|     "PASSWORD": "__SOCKS5PROXY_PASSWORD__" | ||||
|   }, | ||||
|   "PRICE_DATA_SERVER": { | ||||
|     "TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__", | ||||
|     "CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__" | ||||
|   }, | ||||
|   "EXTERNAL_DATA_SERVER": { | ||||
|     "MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__", | ||||
|     "MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__", | ||||
| @ -135,6 +132,10 @@ | ||||
|     "AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__, | ||||
|     "SERVERS": __REPLICATION_SERVERS__ | ||||
|   }, | ||||
|   "MEMPOOL_SERVICES": { | ||||
|     "API": "__MEMPOOL_SERVICES_API__", | ||||
|     "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ | ||||
|   }, | ||||
|   "REDIS": { | ||||
|     "ENABLED": __REDIS_ENABLED__, | ||||
|     "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__" | ||||
|  | ||||
| @ -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_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000} | ||||
| __MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true} | ||||
| 
 | ||||
| __MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1} | ||||
| 
 | ||||
| # CORE_RPC | ||||
| __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} | ||||
| @ -94,10 +94,6 @@ __SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050} | ||||
| __SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""} | ||||
| __SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""} | ||||
| 
 | ||||
| # PRICE_DATA_SERVER | ||||
| __PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices} | ||||
| __PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices} | ||||
| 
 | ||||
| # EXTERNAL_DATA_SERVER | ||||
| __EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1} | ||||
| __EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1} | ||||
| @ -137,6 +133,10 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true} | ||||
| __REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000} | ||||
| __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} | ||||
| 
 | ||||
| # MEMPOOL_SERVICES | ||||
| __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""} | ||||
| __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} | ||||
| 
 | ||||
| # REDIS | ||||
| __REDIS_ENABLED__=${REDIS_ENABLED:=true} | ||||
| __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} | ||||
| @ -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_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json | ||||
| 
 | ||||
| sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json | ||||
| sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json | ||||
| @ -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_PASSWORD__!${__SOCKS5PROXY_PASSWORD__}!g" mempool-config.json | ||||
| 
 | ||||
| sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json | ||||
| sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json | ||||
| 
 | ||||
| sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json | ||||
| sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json | ||||
| sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json | ||||
| @ -267,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_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json | ||||
| 
 | ||||
| # MEMPOOL_SERVICES | ||||
| sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json | ||||
| sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json | ||||
| 
 | ||||
| # REDIS | ||||
| sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json | ||||
| sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json | ||||
|  | ||||
| @ -22,5 +22,6 @@ | ||||
|   "TESTNET_BLOCK_AUDIT_START_HEIGHT": 0, | ||||
|   "SIGNET_BLOCK_AUDIT_START_HEIGHT": 0, | ||||
|   "LIGHTNING": false, | ||||
|   "HISTORICAL_PRICE": true | ||||
|   "HISTORICAL_PRICE": true, | ||||
|   "ACCELERATOR": false | ||||
| } | ||||
|  | ||||
| @ -31,6 +31,14 @@ | ||||
|     <track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null"> | ||||
|   </video> | ||||
| 
 | ||||
|   <ng-container *ngIf="false && officialMempoolSpace"> | ||||
|     <h3 class="mt-5">Sponsor the project</h3> | ||||
|     <div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px"> | ||||
|       <a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a> | ||||
|       <a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a> | ||||
|     </div> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <div class="enterprise-sponsor" id="enterprise-sponsors"> | ||||
|     <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> | ||||
|     <div class="wrapper"> | ||||
| @ -191,20 +199,45 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="community-sponsor" id="community-sponsors"> | ||||
|     <h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3> | ||||
| 
 | ||||
|   <ng-container *ngIf="officialMempoolSpace"> | ||||
|     <div *ngIf="profiles$ | async as profiles" id="community-sponsors"> | ||||
|       <div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0"> | ||||
|         <h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3> | ||||
|         <div class="wrapper"> | ||||
|       <ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors"> | ||||
|         <ng-template ngFor let-sponsor [ngForOf]="sponsors"> | ||||
|           <a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle"> | ||||
|             <img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" /> | ||||
|           <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"> | ||||
|       <ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors"> | ||||
|         <a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle"> | ||||
|           <img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> | ||||
|         </a> | ||||
|       </ng-container> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="community-integrations-sponsor" id="community-integrations"> | ||||
|     <h3 i18n="about.community-integrations">Community Integrations</h3> | ||||
|     <div class="wrapper"> | ||||
| @ -340,7 +373,7 @@ | ||||
|       <div class="wrapper"> | ||||
|         <ng-template ngFor let-translator [ngForOf]="translators"> | ||||
|           <a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key"> | ||||
|             <img class="image" [src]="'/api/v1/translators/images/' + translator.value" /> | ||||
|             <img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> | ||||
|           </a> | ||||
|         </ng-template> | ||||
|       </div> | ||||
| @ -354,7 +387,7 @@ | ||||
|       <div class="wrapper"> | ||||
|         <ng-template ngFor let-contributor [ngForOf]="contributors.regular"> | ||||
|           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> | ||||
|             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" /> | ||||
|             <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> | ||||
|           </a> | ||||
|         </ng-template> | ||||
| @ -366,7 +399,7 @@ | ||||
|       <div class="wrapper"> | ||||
|         <ng-template ngFor let-contributor [ngForOf]="contributors.core"> | ||||
|           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> | ||||
|             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" /> | ||||
|             <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> | ||||
|           </a> | ||||
|         </ng-template> | ||||
|  | ||||
| @ -10,6 +10,9 @@ | ||||
|     margin: 25px; | ||||
|     line-height: 32px; | ||||
|   } | ||||
|   .unknown { | ||||
|     border: 1px solid #b4b4b4; | ||||
|   } | ||||
| 
 | ||||
|   .image.not-rounded { | ||||
|     border-radius: 0; | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { Observable } from 'rxjs'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { IBackendInfo } from '../../interfaces/websocket.interface'; | ||||
| import { Router, ActivatedRoute } from '@angular/router'; | ||||
| import { map, tap } from 'rxjs/operators'; | ||||
| import { map, share, tap } from 'rxjs/operators'; | ||||
| import { ITranslators } from '../../interfaces/node-api.interface'; | ||||
| import { DOCUMENT } from '@angular/common'; | ||||
| 
 | ||||
| @ -19,14 +19,16 @@ import { DOCUMENT } from '@angular/common'; | ||||
| export class AboutComponent implements OnInit { | ||||
|   @ViewChild('promoVideo') promoVideo: ElementRef; | ||||
|   backendInfo$: Observable<IBackendInfo>; | ||||
|   sponsors$: Observable<any>; | ||||
|   translators$: Observable<ITranslators>; | ||||
|   allContributors$: Observable<any>; | ||||
|   frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH; | ||||
|   packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION; | ||||
|   officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; | ||||
|   showNavigateToSponsor = false; | ||||
| 
 | ||||
|   profiles$: Observable<any>; | ||||
|   translators$: Observable<ITranslators>; | ||||
|   allContributors$: Observable<any>; | ||||
|   ogs$: Observable<any>; | ||||
| 
 | ||||
|   constructor( | ||||
|     private websocketService: WebsocketService, | ||||
|     private seoService: SeoService, | ||||
| @ -43,10 +45,13 @@ export class AboutComponent implements OnInit { | ||||
|     this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`); | ||||
|     this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|     this.sponsors$ = this.apiService.getDonation$() | ||||
|       .pipe( | ||||
|         tap(() => this.goToAnchor()) | ||||
|       ); | ||||
|     this.profiles$ = this.apiService.getAboutPageProfiles$().pipe( | ||||
|       tap(() => { | ||||
|         this.goToAnchor() | ||||
|       }), | ||||
|       share(), | ||||
|     ) | ||||
| 
 | ||||
|     this.translators$ = this.apiService.getTranslators$() | ||||
|       .pipe( | ||||
|         map((translators) => { | ||||
| @ -59,6 +64,9 @@ export class AboutComponent implements OnInit { | ||||
|         }), | ||||
|         tap(() => this.goToAnchor()) | ||||
|       ); | ||||
| 
 | ||||
|     this.ogs$ = this.apiService.getOgs$(); | ||||
| 
 | ||||
|     this.allContributors$ = this.apiService.getContributor$().pipe( | ||||
|       map((contributors) => { | ||||
|         return { | ||||
|  | ||||
| @ -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) { | ||||
|       this.scene.update(add, remove, change, direction, resetLayout); | ||||
|       this.start(); | ||||
|  | ||||
| @ -150,7 +150,7 @@ export default class BlockScene { | ||||
|     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 removed = this.removeBatch(remove, startTime, direction); | ||||
| 
 | ||||
| @ -175,6 +175,7 @@ export default class BlockScene { | ||||
|       // update effective rates
 | ||||
|       change.forEach(tx => { | ||||
|         if (this.txs[tx.txid]) { | ||||
|           this.txs[tx.txid].acc = tx.acc; | ||||
|           this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize); | ||||
|           this.txs[tx.txid].rate = tx.rate; | ||||
|           this.txs[tx.txid].dirty = true; | ||||
|  | ||||
| @ -17,6 +17,7 @@ const auditColors = { | ||||
|   missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), | ||||
|   added: hexToColor('0099ff'), | ||||
|   selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), | ||||
|   accelerated: hexToColor('8F5FF6'), | ||||
| }; | ||||
| 
 | ||||
| // convert from this class's update format to TxSprite's update format
 | ||||
| @ -37,8 +38,9 @@ export default class TxView implements TransactionStripped { | ||||
|   vsize: number; | ||||
|   value: number; | ||||
|   feerate: number; | ||||
|   acc?: boolean; | ||||
|   rate?: number; | ||||
|   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; | ||||
|   status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated'; | ||||
|   context?: 'projected' | 'actual'; | ||||
|   scene?: BlockScene; | ||||
| 
 | ||||
| @ -63,6 +65,7 @@ export default class TxView implements TransactionStripped { | ||||
|     this.vsize = tx.vsize; | ||||
|     this.value = tx.value; | ||||
|     this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
 | ||||
|     this.acc = tx.acc; | ||||
|     this.rate = tx.rate; | ||||
|     this.status = tx.status; | ||||
|     this.initialised = false; | ||||
| @ -199,6 +202,11 @@ export default class TxView implements TransactionStripped { | ||||
|     const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; | ||||
|     // Normal mode
 | ||||
|     if (!this.scene?.highlightingEnabled) { | ||||
|       if (this.acc) { | ||||
|         return auditColors.accelerated; | ||||
|       } else { | ||||
|         return feeLevelColor; | ||||
|       } | ||||
|       return feeLevelColor; | ||||
|     } | ||||
|     // Block audit
 | ||||
| @ -216,6 +224,8 @@ export default class TxView implements TransactionStripped { | ||||
|         return auditColors.added; | ||||
|       case 'selected': | ||||
|         return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||
|       case 'accelerated': | ||||
|         return auditColors.accelerated; | ||||
|       case 'found': | ||||
|         if (this.context === 'projected') { | ||||
|           return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; | ||||
| @ -223,10 +233,14 @@ export default class TxView implements TransactionStripped { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|       default: | ||||
|         if (this.acc) { | ||||
|           return auditColors.accelerated; | ||||
|         } else { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function hexToColor(hex: string): Color { | ||||
|   return { | ||||
|  | ||||
| @ -29,7 +29,8 @@ | ||||
|         </td> | ||||
|       </tr> | ||||
|       <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> | ||||
|           <app-fee-rate [fee]="effectiveRate"></app-fee-rate> | ||||
|         </td> | ||||
| @ -54,6 +55,7 @@ | ||||
|           <td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td> | ||||
|           <td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td> | ||||
|           <td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td> | ||||
|           <td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td> | ||||
|         </ng-container> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|  | ||||
| @ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges { | ||||
|   vsize = 1; | ||||
|   feeRate = 0; | ||||
|   effectiveRate; | ||||
|   acceleration; | ||||
| 
 | ||||
|   tooltipPosition: Position = { x: 0, y: 0 }; | ||||
| 
 | ||||
| @ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges { | ||||
|       this.vsize = tx.vsize || 1; | ||||
|       this.feeRate = this.fee / this.vsize; | ||||
|       this.effectiveRate = tx.rate; | ||||
|       this.acceleration = tx.acc; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -340,12 +340,16 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         const isFresh = {}; | ||||
|         const isSigop = {}; | ||||
|         const isRbf = {}; | ||||
|         const isAccelerated = {}; | ||||
|         this.numMissing = 0; | ||||
|         this.numUnexpected = 0; | ||||
| 
 | ||||
|         if (blockAudit?.template) { | ||||
|           for (const tx of blockAudit.template) { | ||||
|             inTemplate[tx.txid] = true; | ||||
|             if (tx.acc) { | ||||
|               isAccelerated[tx.txid] = true; | ||||
|             } | ||||
|           } | ||||
|           for (const tx of transactions) { | ||||
|             inBlock[tx.txid] = true; | ||||
| @ -365,6 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|           for (const txid of blockAudit.fullrbfTxs || []) { | ||||
|             isRbf[txid] = true; | ||||
|           } | ||||
|           for (const txid of blockAudit.acceleratedTxs || []) { | ||||
|             isAccelerated[txid] = true; | ||||
|           } | ||||
|           // set transaction statuses
 | ||||
|           for (const tx of blockAudit.template) { | ||||
|             tx.context = 'projected'; | ||||
| @ -389,6 +396,9 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|               isMissing[tx.txid] = true; | ||||
|               this.numMissing++; | ||||
|             } | ||||
|             if (isAccelerated[tx.txid]) { | ||||
|               tx.status = 'accelerated'; | ||||
|             } | ||||
|           } | ||||
|           for (const [index, tx] of transactions.entries()) { | ||||
|             tx.context = 'actual'; | ||||
| @ -405,6 +415,9 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|               isSelected[tx.txid] = true; | ||||
|               this.numUnexpected++; | ||||
|             } | ||||
|             if (isAccelerated[tx.txid]) { | ||||
|               tx.status = 'accelerated'; | ||||
|             } | ||||
|           } | ||||
|           for (const tx of transactions) { | ||||
|             inBlock[tx.txid] = true; | ||||
|  | ||||
| @ -4,38 +4,56 @@ | ||||
|   class="difficulty-tooltip" | ||||
|   [style.visibility]="status ? 'visible' : 'hidden'" | ||||
|   [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 *ngIf="isAhead"> | ||||
|       <ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container> | ||||
|       <ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template> | ||||
|       <ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template> | ||||
|       <ng-container *ngTemplateOutlet="expected === 1 ? expectedMinedBlocksSingular : expectedMinedBlocksPlural; context: {$implicit: expected }"></ng-container> | ||||
|     </ng-container> | ||||
|     <ng-container *ngIf="!isAhead"> | ||||
|       <ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container> | ||||
|       <ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template> | ||||
|       <ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template> | ||||
|       <ng-container *ngTemplateOutlet="mined === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: mined }"></ng-container> | ||||
|     </ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'remaining'"> | ||||
|     <ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container> | ||||
|     <ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template> | ||||
|     <ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template> | ||||
|     <ng-container *ngTemplateOutlet="remaining === 1 ? remainingBlocksSingular : remainingBlocksPlural; context: {$implicit: remaining }"></ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'ahead'"> | ||||
|     <ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container> | ||||
|     <ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template> | ||||
|     <ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template> | ||||
|     <ng-container *ngTemplateOutlet="ahead === 1 ? aheadBlocksSingular : aheadBlocksPlural; context: {$implicit: ahead }"></ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'behind'"> | ||||
|     <ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container> | ||||
|     <ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template> | ||||
|     <ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template> | ||||
|     <ng-container *ngTemplateOutlet="behind === 1 ? behindBlocksSingular : behindBlocksPlural; context: {$implicit: behind }"></ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngSwitchCase="'next'"> | ||||
|     <span class="next-block" i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">Next Block</span> | ||||
|   </ng-container> | ||||
| </ng-container> | ||||
| <ng-container *ngIf="isMobile"> | ||||
|   <ng-container *ngIf="isAhead"> | ||||
|     <ng-container *ngTemplateOutlet="expected === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: expected }"></ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngIf="!isAhead"> | ||||
|     <ng-container *ngTemplateOutlet="mined === 1 ? minedBlocksSingular : minedBlocksPlural; context: {$implicit: mined }"></ng-container> | ||||
|   </ng-container> | ||||
|   <br> | ||||
|   <ng-container *ngTemplateOutlet="remaining === 1 ? remainingBlocksSingular : remainingBlocksPlural; context: {$implicit: remaining }"></ng-container> | ||||
|   <br> | ||||
|   <ng-container *ngIf="ahead > 0"> | ||||
|     <ng-container *ngTemplateOutlet="ahead === 1 ? aheadBlocksSingular : aheadBlocksPlural; context: {$implicit: ahead }"></ng-container> | ||||
|   </ng-container> | ||||
|   <ng-container *ngIf="behind > 0"> | ||||
|     <ng-container *ngTemplateOutlet="behind === 1 ? behindBlocksSingular : behindBlocksPlural; context: {$implicit: behind }"></ng-container> | ||||
|   </ng-container> | ||||
| </ng-container> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #expectedMinedBlocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template> | ||||
| <ng-template #expectedMinedBlocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template> | ||||
| <ng-template #minedBlocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template> | ||||
| <ng-template #minedBlocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template> | ||||
| <ng-template #remainingBlocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template> | ||||
| <ng-template #remainingBlocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template> | ||||
| <ng-template #aheadBlocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template> | ||||
| <ng-template #aheadBlocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template> | ||||
| <ng-template #behindBlocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template> | ||||
| <ng-template #behindBlocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template> | ||||
| @ -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 { | ||||
|   base: string; | ||||
| @ -35,12 +35,15 @@ export class DifficultyTooltipComponent implements OnChanges { | ||||
|   remaining: number; | ||||
|   isAhead: boolean; | ||||
|   isBehind: boolean; | ||||
|   isMobile: boolean; | ||||
| 
 | ||||
|   tooltipPosition = { x: 0, y: 0 }; | ||||
| 
 | ||||
|   @ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>; | ||||
| 
 | ||||
|   constructor() {} | ||||
|   constructor() { | ||||
|     this.onResize(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes): void { | ||||
|     if (changes.cursorPosition && changes.cursorPosition.currentValue) { | ||||
| @ -63,4 +66,9 @@ export class DifficultyTooltipComponent implements OnChanges { | ||||
|       this.isBehind = this.behind > 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
|     <div class="card-body more-padding"> | ||||
|       <div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty"> | ||||
|         <div class="epoch-progress"> | ||||
|           <svg 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> | ||||
|               <linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse"> | ||||
|                 <stop offset="0%" stop-color="#105fb0" /> | ||||
| @ -22,7 +22,7 @@ | ||||
|               class="rect {{rect.status}}" | ||||
|               [class.hover]="hoverSection && rect.status === hoverSection.status" | ||||
|               (pointerover)="onHover($event, rect);" | ||||
|               (pointerout)="onBlur($event);" | ||||
|               (pointerout)="onBlur();" | ||||
|             > | ||||
|               <animate | ||||
|                 *ngIf="rect.status === 'next'" | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; | ||||
| import { combineLatest, Observable, timer } from 'rxjs'; | ||||
| import { map, switchMap } from 'rxjs/operators'; | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, ElementRef, ViewChild, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; | ||||
| import { combineLatest, Observable } from 'rxjs'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| import { StateService } from '../..//services/state.service'; | ||||
| 
 | ||||
| interface EpochProgress { | ||||
| @ -45,6 +45,8 @@ export class DifficultyComponent implements OnInit { | ||||
|   @Input() showHalving = false; | ||||
|   @Input() showTitle = true; | ||||
| 
 | ||||
|   @ViewChild('epochSvg') epochSvgElement: ElementRef<SVGElement>; | ||||
|   | ||||
|   isLoadingWebSocket$: Observable<boolean>; | ||||
|   difficultyEpoch$: Observable<EpochProgress>; | ||||
| 
 | ||||
| @ -191,21 +193,26 @@ export class DifficultyComponent implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('pointerdown', ['$event']) | ||||
|   onPointerDown(event) { | ||||
|   onPointerDown(event): void { | ||||
|     if (this.epochSvgElement.nativeElement?.contains(event.target)) { | ||||
|       this.onPointerMove(event); | ||||
|       event.preventDefault(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('pointermove', ['$event']) | ||||
|   onPointerMove(event) { | ||||
|   onPointerMove(event): void { | ||||
|     if (this.epochSvgElement.nativeElement?.contains(event.target)) { | ||||
|       this.tooltipPosition = { x: event.clientX, y: event.clientY }; | ||||
|       this.cd.markForCheck(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onHover(event, rect): void { | ||||
|   onHover(_, rect): void { | ||||
|     this.hoverSection = rect; | ||||
|   } | ||||
| 
 | ||||
|   onBlur(event): void { | ||||
|   onBlur(): void { | ||||
|     this.hoverSection = null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -64,7 +64,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr | ||||
|       return; | ||||
|     } | ||||
|     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 sampleInterval = maxBlockVSize / this.numSamples; | ||||
|     let cumVSize = 0; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <ng-container *ngIf="{ val: network$ | async } as network"> | ||||
| <header> | ||||
| <header *ngIf="headerVisible"> | ||||
|   <nav class="navbar navbar-expand-md navbar-dark bg-dark"> | ||||
|   <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)"> | ||||
|   <ng-template [ngIf]="subdomain"> | ||||
|  | ||||
| @ -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 { Observable, merge, of } from 'rxjs'; | ||||
| import { LanguageService } from '../../services/language.service'; | ||||
| @ -11,6 +11,9 @@ import { NavigationService } from '../../services/navigation.service'; | ||||
|   styleUrls: ['./master-page.component.scss'], | ||||
| }) | ||||
| export class MasterPageComponent implements OnInit { | ||||
|   @Input() headerVisible = true; | ||||
|   @Input() footerVisibleOverride: boolean | null = null; | ||||
| 
 | ||||
|   env: Env; | ||||
|   network$: Observable<string>; | ||||
|   connectionState$: Observable<number>; | ||||
| @ -38,11 +41,15 @@ export class MasterPageComponent implements OnInit { | ||||
|     this.subdomain = this.enterpriseService.getSubdomain(); | ||||
|     this.navigationService.subnetPaths.subscribe((paths) => { | ||||
|       this.networkPaths = paths; | ||||
|       if (this.footerVisibleOverride === null) { | ||||
|         if (paths.mainnet.indexOf('docs') > -1) { | ||||
|           this.footerVisible = false; | ||||
|         } else { | ||||
|           this.footerVisible = true; | ||||
|         } | ||||
|       } else { | ||||
|         this.footerVisible = this.footerVisibleOverride; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -94,7 +94,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | ||||
| 
 | ||||
|   updateBlock(delta: MempoolBlockDelta): void { | ||||
|     const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight); | ||||
| 
 | ||||
|     if (this.blockIndex !== this.index) { | ||||
|       const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; | ||||
|       this.blockGraph.replace(delta.added, direction); | ||||
|  | ||||
| @ -49,7 +49,7 @@ | ||||
|         </div> | ||||
|       </ng-template> | ||||
|     </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> | ||||
| </ng-container> | ||||
| 
 | ||||
|  | ||||
| @ -170,3 +170,33 @@ | ||||
|   border-radius: 2px; | ||||
|   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; | ||||
|   } | ||||
| } | ||||
| @ -26,6 +26,7 @@ import { animate, style, transition, trigger } from '@angular/animations'; | ||||
| export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   @Input() minimal: boolean = false; | ||||
|   @Input() blockWidth: number = 125; | ||||
|   @Input() containerWidth: number = null; | ||||
|   @Input() count: number = null; | ||||
|   @Input() spotlight: number = 0; | ||||
|   @Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`; | ||||
|  | ||||
| @ -99,14 +99,20 @@ | ||||
|                     </ng-template> | ||||
|                     <ng-template #estimationTmpl> | ||||
|                       <ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit"> | ||||
|                         <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 #belowBlockLimit> | ||||
|                         <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> | ||||
|                         </ng-template> | ||||
|                         <ng-template #timeEstimateDefault> | ||||
|                           <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> | ||||
| @ -488,7 +494,8 @@ | ||||
|         </td> | ||||
|       </tr> | ||||
|       <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> | ||||
|           <div class="effective-fee-container"> | ||||
|             <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate> | ||||
|  | ||||
| @ -217,3 +217,22 @@ | ||||
|     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; | ||||
|   } | ||||
| } | ||||
| @ -97,7 +97,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     private router: Router, | ||||
|     private relativeUrlPipe: RelativeUrlPipe, | ||||
|     private electrsApiService: ElectrsApiService, | ||||
|     private stateService: StateService, | ||||
|     public stateService: StateService, | ||||
|     private cacheService: CacheService, | ||||
|     private websocketService: WebsocketService, | ||||
|     private audioService: AudioService, | ||||
| @ -183,6 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|         } else { | ||||
|           this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize; | ||||
|         } | ||||
|         if (cpfpInfo.acceleration) { | ||||
|           this.tx.acceleration = cpfpInfo.acceleration; | ||||
|         } | ||||
| 
 | ||||
|         this.cpfpInfo = cpfpInfo; | ||||
|         this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01)); | ||||
|  | ||||
| @ -19,6 +19,7 @@ export interface Transaction { | ||||
|   ancestors?: Ancestor[]; | ||||
|   bestDescendant?: BestDescendant | null; | ||||
|   cpfpChecked?: boolean; | ||||
|   acceleration?: number; | ||||
|   deleteAfter?: number; | ||||
|   _unblinded?: any; | ||||
|   _deduced?: boolean; | ||||
|  | ||||
| @ -27,6 +27,7 @@ export interface CpfpInfo { | ||||
|   effectiveFeePerVsize?: number; | ||||
|   sigops?: number; | ||||
|   adjustedVsize?: number; | ||||
|   acceleration?: number; | ||||
| } | ||||
| 
 | ||||
| export interface RbfInfo { | ||||
| @ -111,6 +112,7 @@ export interface PoolInfo { | ||||
|   addresses: string; // JSON array
 | ||||
|   emptyBlocks: number; | ||||
|   slug: string; | ||||
|   poolUniqueId: number; | ||||
| } | ||||
| export interface PoolStat { | ||||
|   pool: PoolInfo; | ||||
| @ -159,6 +161,7 @@ export interface BlockAudit extends BlockExtended { | ||||
|   freshTxs: string[], | ||||
|   sigopTxs: string[], | ||||
|   fullrbfTxs: string[], | ||||
|   acceleratedTxs: string[], | ||||
|   matchRate: number, | ||||
|   expectedFees: number, | ||||
|   expectedWeight: number, | ||||
| @ -175,7 +178,8 @@ export interface TransactionStripped { | ||||
|   vsize: number; | ||||
|   value: number; | ||||
|   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'; | ||||
| } | ||||
| 
 | ||||
| @ -187,6 +191,7 @@ export interface RbfTransaction extends TransactionStripped { | ||||
| export interface MempoolPosition { | ||||
|   block: number, | ||||
|   vsize: number, | ||||
|   accelerated?: boolean | ||||
| } | ||||
| 
 | ||||
| export interface RewardStats { | ||||
|  | ||||
| @ -70,7 +70,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock { | ||||
| export interface MempoolBlockDelta { | ||||
|   added: TransactionStripped[], | ||||
|   removed: string[], | ||||
|   changed?: { txid: string, rate: number | undefined }[]; | ||||
|   changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[]; | ||||
| } | ||||
| 
 | ||||
| export interface MempoolInfo { | ||||
| @ -88,8 +88,9 @@ export interface TransactionStripped { | ||||
|   fee: number; | ||||
|   vsize: number; | ||||
|   value: number; | ||||
|   acc?: boolean; // is accelerated?
 | ||||
|   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'; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 { NodeChannels } from '../lightning/nodes-channels/node-channels.component'; | ||||
| import { GroupComponent } from './group/group.component'; | ||||
| import { NodeOwnerComponent } from './node-owner/node-owner.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @ -66,6 +67,7 @@ import { GroupComponent } from './group/group.component'; | ||||
|     NodesRankingsDashboard, | ||||
|     NodeChannels, | ||||
|     GroupComponent, | ||||
|     NodeOwnerComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
| @ -103,6 +105,7 @@ import { GroupComponent } from './group/group.component'; | ||||
|     OldestNodes, | ||||
|     NodesRankingsDashboard, | ||||
|     NodeChannels, | ||||
|     NodeOwnerComponent, | ||||
|   ], | ||||
|   providers: [ | ||||
|     LightningApiService, | ||||
|  | ||||
| @ -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> | ||||
| @ -0,0 +1,4 @@ | ||||
| .profile-photo { | ||||
|   width: 31px; | ||||
|   height: 31px; | ||||
| } | ||||
| @ -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 | ||||
|   ) { | ||||
|   } | ||||
| } | ||||
| @ -3,13 +3,17 @@ | ||||
|   <ng-container *ngIf="!error"> | ||||
|     <h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5> | ||||
|     <div class="title-container mb-2"> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|         <h1 class="mb-0 text-truncate">{{ node.alias }}</h1> | ||||
|       <span class="tx-link"> | ||||
|         <!-- <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"> | ||||
|           <app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]"> | ||||
|             <app-clipboard [text]="node.public_key"></app-clipboard> | ||||
|           </app-truncate> | ||||
|         </span> | ||||
|         <!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn-mobile"></app-node-owner> --> | ||||
|       </span> | ||||
|     </div> | ||||
|   </ng-container> | ||||
|  | ||||
| @ -111,3 +111,17 @@ app-fiat { | ||||
|   margin: 0 0.25em; | ||||
|   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; | ||||
|   } | ||||
| } | ||||
| @ -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 { Observable } from 'rxjs'; | ||||
| import { catchError, map, switchMap, tap } from 'rxjs/operators'; | ||||
| import { Observable, of, EMPTY } from 'rxjs'; | ||||
| import { catchError, map, switchMap, tap, share } from 'rxjs/operators'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { LightningApiService } from '../lightning-api.service'; | ||||
| @ -38,6 +38,7 @@ export class NodeComponent implements OnInit { | ||||
|   tlvRecords: CustomRecord[]; | ||||
|   avgChannelDistance$: Observable<number | null>; | ||||
|   showFeatures = false; | ||||
|   nodeOwner$: Observable<any>; | ||||
|   kmToMiles = kmToMiles; | ||||
| 
 | ||||
|   constructor( | ||||
| @ -45,6 +46,7 @@ export class NodeComponent implements OnInit { | ||||
|     private lightningApiService: LightningApiService, | ||||
|     private activatedRoute: ActivatedRoute, | ||||
|     private seoService: SeoService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
| @ -147,6 +149,24 @@ export class NodeComponent implements OnInit { | ||||
|         return 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 { | ||||
|  | ||||
| @ -8,6 +8,8 @@ import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||
| import { Outspend, Transaction } from '../interfaces/electrs.interface'; | ||||
| import { Conversion } from './price.service'; | ||||
| 
 | ||||
| const SERVICES_API_PREFIX = `/api/v1/services`; | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| @ -92,15 +94,11 @@ export class ApiService { | ||||
|     return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params }); | ||||
|   } | ||||
| 
 | ||||
|   requestDonation$(amount: number, orderId: string): Observable<any> { | ||||
|     const params = { | ||||
|       amount: amount, | ||||
|       orderId: orderId, | ||||
|     }; | ||||
|     return this.httpClient.post<any>(this.apiBaseUrl + '/api/v1/donations', params); | ||||
|   getAboutPageProfiles$(): Observable<any[]> { | ||||
|     return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/about-page'); | ||||
|   } | ||||
| 
 | ||||
|   getDonation$(): Observable<any[]> { | ||||
|   getOgs$(): Observable<any> { | ||||
|     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'); | ||||
|   } | ||||
| 
 | ||||
|   checkDonation$(orderId: string): Observable<any[]> { | ||||
|     return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations/check?order_id=' + orderId); | ||||
|   } | ||||
| 
 | ||||
|   getInitData$(): Observable<WebsocketResponse> { | ||||
|     return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data'); | ||||
|   } | ||||
| @ -323,4 +317,13 @@ export class ApiService { | ||||
|         (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' }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -47,6 +47,7 @@ export interface Env { | ||||
|   TESTNET_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   SIGNET_BLOCK_AUDIT_START_HEIGHT: number; | ||||
|   HISTORICAL_PRICE: boolean; | ||||
|   ACCELERATOR: boolean; | ||||
| } | ||||
| 
 | ||||
| const defaultEnv: Env = { | ||||
| @ -77,6 +78,7 @@ const defaultEnv: Env = { | ||||
|   'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0, | ||||
|   'HISTORICAL_PRICE': true, | ||||
|   'ACCELERATOR': false, | ||||
| }; | ||||
| 
 | ||||
| @Injectable({ | ||||
|  | ||||
| @ -28,8 +28,9 @@ export class WebsocketService { | ||||
|   private isTrackingTx = false; | ||||
|   private trackingTxId: string; | ||||
|   private isTrackingMempoolBlock = false; | ||||
|   private isTrackingRbf = false; | ||||
|   private isTrackingRbf: 'all' | 'fullRbf' | false = false; | ||||
|   private isTrackingRbfSummary = false; | ||||
|   private isTrackingAddress: string | false = false; | ||||
|   private trackingMempoolBlock: number; | ||||
|   private latestGitCommit = ''; | ||||
|   private onlineCheckTimeout: number; | ||||
| @ -110,6 +111,15 @@ export class WebsocketService { | ||||
|           if (this.isTrackingMempoolBlock) { | ||||
|             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); | ||||
|         } | ||||
| 
 | ||||
| @ -151,10 +161,12 @@ export class WebsocketService { | ||||
| 
 | ||||
|   startTrackAddress(address: string) { | ||||
|     this.websocketSubject.next({ 'track-address': address }); | ||||
|     this.isTrackingAddress = address; | ||||
|   } | ||||
| 
 | ||||
|   stopTrackingAddress() { | ||||
|     this.websocketSubject.next({ 'track-address': 'stop' }); | ||||
|     this.isTrackingAddress = false; | ||||
|   } | ||||
| 
 | ||||
|   startTrackAsset(asset: string) { | ||||
| @ -178,7 +190,7 @@ export class WebsocketService { | ||||
| 
 | ||||
|   startTrackRbf(mode: 'all' | 'fullRbf') { | ||||
|     this.websocketSubject.next({ 'track-rbf': mode }); | ||||
|     this.isTrackingRbf = true; | ||||
|     this.isTrackingRbf = mode; | ||||
|   } | ||||
| 
 | ||||
|   stopTrackRbf() { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <footer> | ||||
|     <div class="container-fluid"> | ||||
|     <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"> | ||||
|           <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> | ||||
| @ -16,10 +16,12 @@ | ||||
|         <div class="selector"> | ||||
|           <app-rate-unit-selector></app-rate-unit-selector> | ||||
|         </div> | ||||
|         <ng-template #temporaryHidden> | ||||
|           <a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a> | ||||
|           <p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p> | ||||
|         </ng-template> | ||||
|         <div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta"> | ||||
|           <a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]"> | ||||
|             <span *ngIf="loggedIn" i18n="shared.my-account">My Account</span> | ||||
|             <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span> | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="col-lg-6 col-md-10 offset-md-1 links outer"> | ||||
|         <div class="row"> | ||||
|  | ||||
| @ -22,7 +22,7 @@ footer .row.main .branding { | ||||
| } | ||||
| 
 | ||||
| footer .row.main .branding > p { | ||||
|   margin-bottom: 45px; | ||||
|   margin-bottom: 25px; | ||||
| } | ||||
| 
 | ||||
| footer .row.main .branding .btn { | ||||
| @ -35,11 +35,7 @@ footer .row.main .branding button.account { | ||||
| } | ||||
| 
 | ||||
| footer .row.main .branding .cta { | ||||
|   margin: 20px auto 25px auto; | ||||
| } | ||||
| 
 | ||||
| footer .row.main .branding .cta-secondary { | ||||
| 
 | ||||
|   margin: 25px auto 25px auto; | ||||
| } | ||||
| 
 | ||||
| footer .row.main .links.outer { | ||||
|  | ||||
| @ -1,10 +1,13 @@ | ||||
| import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core'; | ||||
| import { Observable, merge, of, Subject } from 'rxjs'; | ||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { Observable, merge, of, Subject, Subscription } from 'rxjs'; | ||||
| import { tap, takeUntil } from 'rxjs/operators'; | ||||
| import { Env, StateService } from '../../../services/state.service'; | ||||
| import { IBackendInfo } from '../../../interfaces/websocket.interface'; | ||||
| import { LanguageService } from '../../../services/language.service'; | ||||
| import { NavigationService } from '../../../services/navigation.service'; | ||||
| import { StorageService } from '../../../services/storage.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-global-footer', | ||||
| @ -23,12 +26,19 @@ export class GlobalFooterComponent implements OnInit { | ||||
|   network$: Observable<string>; | ||||
|   networkPaths: { [network: string]: string }; | ||||
|   currentNetwork = ''; | ||||
|   loggedIn = false; | ||||
|   username = null; | ||||
|   urlSubscription: Subscription; | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private languageService: LanguageService, | ||||
|     private navigationService: NavigationService, | ||||
|     @Inject(LOCALE_ID) public locale: string, | ||||
|     private storageService: StorageService, | ||||
|     private route: ActivatedRoute, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     private websocketService: WebsocketService | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
| @ -46,11 +56,23 @@ export class GlobalFooterComponent implements OnInit { | ||||
|     this.network$.pipe(takeUntil(this.destroy$)).subscribe((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 { | ||||
|     this.destroy$.next(true); | ||||
|     this.destroy$.complete(); | ||||
|     this.urlSubscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   networkLink(network) { | ||||
|  | ||||
| @ -219,6 +219,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AmountShortenerPipe, | ||||
|   ], | ||||
|   exports: [ | ||||
|     MasterPageComponent, | ||||
|     RouterModule, | ||||
|     ReactiveFormsModule, | ||||
|     NgbNavModule, | ||||
|  | ||||
							
								
								
									
										1
									
								
								frontend/src/resources/profile/unknown.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/unknown.svg
									
									
									
									
									
										Normal 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 | 
| @ -18,7 +18,8 @@ | ||||
|     "USE_SECOND_NODE_FOR_MINFEE": true, | ||||
|     "DISK_CACHE_BLOCK_INTERVAL": 1, | ||||
|     "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, | ||||
|     "ALLOW_UNREACHABLE": true | ||||
|     "ALLOW_UNREACHABLE": true, | ||||
|     "PRICE_UPDATES_PER_HOUR": 12 | ||||
|   }, | ||||
|   "SYSLOG" : { | ||||
|     "MIN_PRIORITY": "debug" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user