Merge branch 'master' into nymkappa/no-db-blocks-list
This commit is contained in:
		
						commit
						4d6c5a7ce7
					
				
							
								
								
									
										16
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,7 @@ | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: npm | ||||
|     versioning-strategy: increase | ||||
|     directory: "/backend" | ||||
|     schedule: | ||||
|       interval: daily | ||||
| @ -14,6 +15,21 @@ updates: | ||||
| 
 | ||||
|   - package-ecosystem: npm | ||||
|     directory: "/frontend" | ||||
|     versioning-strategy: increase | ||||
|     groups: | ||||
|       frontend-angular-dependencies: | ||||
|         patterns: | ||||
|           - "@angular*" | ||||
|           - "@ng-*" | ||||
|           - "ngx-*" | ||||
|       frontend-jest-dependencies: | ||||
|         patterns: | ||||
|           - "@types/jest" | ||||
|           - "jest" | ||||
|       frontend-eslint-dependencies: | ||||
|         patterns: | ||||
|           - "@typescript-eslint*" | ||||
|           - "eslint" | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|  | ||||
							
								
								
									
										15
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -27,8 +27,17 @@ jobs: | ||||
|           node-version: ${{ matrix.node }} | ||||
|           registry-url: "https://registry.npmjs.org" | ||||
| 
 | ||||
|       - name: Install 1.70.x Rust toolchain | ||||
|         uses: dtolnay/rust-toolchain@1.70 | ||||
|       - name: Read rust-toolchain file from repository | ||||
|         id: gettoolchain | ||||
|         run: echo "::set-output name=toolchain::$(cat rust-toolchain)" | ||||
|         working-directory: ${{ matrix.node }}/${{ matrix.flavor }} | ||||
| 
 | ||||
|       - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain | ||||
|         # Latest version available on this commit is 1.71.1 | ||||
|         # Commit date is Aug 3, 2023 | ||||
|         uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06 | ||||
|         with: | ||||
|           toolchain: ${{ steps.gettoolchain.outputs.toolchain }} | ||||
| 
 | ||||
|       - name: Install | ||||
|         if: ${{ matrix.flavor == 'dev'}} | ||||
| @ -47,7 +56,7 @@ jobs: | ||||
| 
 | ||||
|       - name: Unit Tests | ||||
|         if: ${{ matrix.flavor == 'dev'}} | ||||
|         run: npm run test | ||||
|         run: npm run test:ci | ||||
|         working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend | ||||
| 
 | ||||
|       - name: Build | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cypress.yml
									
									
									
									
										vendored
									
									
								
							| @ -38,7 +38,7 @@ jobs: | ||||
|       - name: Setup node | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16.15.0 | ||||
|           node-version: 18 | ||||
|           cache: "npm" | ||||
|           cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -45,3 +45,6 @@ testem.log | ||||
| #System Files | ||||
| .DS_Store | ||||
| Thumbs.db | ||||
| 
 | ||||
| # package folder (npm run package output) | ||||
| /package | ||||
|  | ||||
| @ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend: | ||||
| 
 | ||||
| ``` | ||||
| cd backend | ||||
| npm install | ||||
| npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links | ||||
| npm run build | ||||
| ``` | ||||
| 
 | ||||
|  | ||||
| @ -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", | ||||
| @ -49,7 +50,8 @@ | ||||
|   "ESPLORA": { | ||||
|     "REST_API_URL": "http://127.0.0.1:3000", | ||||
|     "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", | ||||
|     "RETRY_UNIX_SOCKET_AFTER": 30000 | ||||
|     "RETRY_UNIX_SOCKET_AFTER": 30000, | ||||
|     "FALLBACK": [] | ||||
|   }, | ||||
|   "SECOND_CORE_RPC": { | ||||
|     "HOST": "127.0.0.1", | ||||
| @ -115,10 +117,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 +135,9 @@ | ||||
|       "trusted", | ||||
|       "servers" | ||||
|     ] | ||||
|   }, | ||||
|   "MEMPOOL_SERVICES": { | ||||
|     "API": "https://mempool.space/api", | ||||
|     "ACCELERATIONS": false | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										17
									
								
								backend/npm_package.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								backend/npm_package.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,17 @@ | ||||
| #/bin/sh | ||||
| set -e | ||||
| 
 | ||||
| # Remove previous dist folder | ||||
| rm -rf dist | ||||
| # Build new dist folder | ||||
| npm run build | ||||
| # Remove previous package folder | ||||
| rm -rf package | ||||
| # Move JS and deps | ||||
| mv dist package | ||||
| cp -R node_modules package | ||||
| # Remove symlink for rust-gbt and insert real folder | ||||
| rm package/node_modules/rust-gbt | ||||
| cp -R rust-gbt package/node_modules | ||||
| # Clean up deps | ||||
| npm run package-rm-build-deps | ||||
							
								
								
									
										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,19 +22,20 @@ | ||||
|   "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 rust-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", | ||||
|     "reindex-all-blocks": "npm run start-production --update-pools --reindex-blocks", | ||||
|     "test": "./node_modules/.bin/jest --coverage", | ||||
|     "test:ci": "CI=true ./node_modules/.bin/jest --coverage", | ||||
|     "lint": "./node_modules/.bin/eslint . --ext .ts", | ||||
|     "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", | ||||
|     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", | ||||
|     "build-rust": "cd rust-gbt && npm install" | ||||
|     "rust-build": "cd rust-gbt && npm run build-release" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.21.3", | ||||
| @ -45,7 +46,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); | ||||
| @ -328,13 +335,15 @@ fn set_relatives(txid: u32, audit_pool: &mut AuditPool) { | ||||
|     let mut total_sigops: u32 = 0; | ||||
| 
 | ||||
|     for ancestor_id in &ancestors { | ||||
|         let Some(ancestor) = audit_pool | ||||
|         if let Some(ancestor) = audit_pool | ||||
|             .get(*ancestor_id as usize) | ||||
|             .expect("audit_pool contains all ancestors") else { todo!() }; | ||||
|         total_fee += ancestor.fee; | ||||
|         total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight; | ||||
|         total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize; | ||||
|         total_sigops += ancestor.sigops; | ||||
|             .expect("audit_pool contains all ancestors") | ||||
|         { | ||||
|             total_fee += ancestor.fee; | ||||
|             total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight; | ||||
|             total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize; | ||||
|             total_sigops += ancestor.sigops; | ||||
|         } else { todo!() }; | ||||
|     } | ||||
| 
 | ||||
|     if let Some(Some(tx)) = audit_pool.get_mut(txid as usize) { | ||||
|  | ||||
| @ -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__", | ||||
| @ -50,7 +51,8 @@ | ||||
|   "ESPLORA": { | ||||
|     "REST_API_URL": "__ESPLORA_REST_API_URL__", | ||||
|     "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", | ||||
|     "RETRY_UNIX_SOCKET_AFTER": 888 | ||||
|     "RETRY_UNIX_SOCKET_AFTER": 888, | ||||
|     "FALLBACK": [] | ||||
|   }, | ||||
|   "SECOND_CORE_RPC": { | ||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", | ||||
| @ -92,10 +94,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 +127,10 @@ | ||||
|     "AUDIT_START_HEIGHT": 774000, | ||||
|     "SERVERS": [] | ||||
|   }, | ||||
|   "MEMPOOL_SERVICES": { | ||||
|     "API": "", | ||||
|     "ACCELERATIONS": false | ||||
|   }, | ||||
|   "REDIS": { | ||||
|     "ENABLED": false, | ||||
|     "UNIX_SOCKET_PATH": "/tmp/redis.sock" | ||||
|  | ||||
							
								
								
									
										24
									
								
								backend/src/__tests__/api/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								backend/src/__tests__/api/common.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| import { Common } from '../../api/common'; | ||||
| import { MempoolTransactionExtended } from '../../mempool.interfaces'; | ||||
| 
 | ||||
| const randomTransactions = require('./test-data/transactions-random.json'); | ||||
| const replacedTransactions = require('./test-data/transactions-replaced.json'); | ||||
| const rbfTransactions = require('./test-data/transactions-rbfs.json'); | ||||
| 
 | ||||
| describe('Mempool Utils', () => { | ||||
|   test('should detect RBF transactions with fast method', () => { | ||||
|     const newTransactions = rbfTransactions.concat(randomTransactions); | ||||
|     const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); | ||||
|     expect(Object.values(result).length).toEqual(2); | ||||
|     expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); | ||||
|     expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); | ||||
|   }); | ||||
| 
 | ||||
|   test.only('should detect RBF transactions with scalable method', () => { | ||||
|     const newTransactions = rbfTransactions.concat(randomTransactions); | ||||
|     const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); | ||||
|     expect(Object.values(result).length).toEqual(2); | ||||
|     expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); | ||||
|     expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										277
									
								
								backend/src/__tests__/api/test-data/transactions-random.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								backend/src/__tests__/api/test-data/transactions-random.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,277 @@ | ||||
| [ | ||||
|     { | ||||
|         "txid": "13f007241d78e8b0b4e57d2ae3fd37bcfe3226534d7cadeba5a549860d960db0", | ||||
|         "version": 2, | ||||
|         "locktime": 0, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "cb8f206f4e88bec97107089f3e9e61d50cde53d4541992ae19759b71103cf75c", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014fd6d15ff832c12f1ff04a5ccd5039f7227b260bd", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 fd6d15ff832c12f1ff04a5ccd5039f7227b260bd", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1ql4k3tlur9sf0rlcy5hxd2qulwgnmyc9akehvth", | ||||
|                     "value": 610677 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304302205c430b36ebd2bb327951d83440af1f58f127871b2baada4c4dde2bc0b6721f56021f3445099f1a40e35baeda32e8e3727b505ffba0d882b11f498c7762f4184e9901", | ||||
|                     "0236b5edd4fbbcfb045960e42ec8a9968944084785932e32940e8cd2583b37da67" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 2147483648 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd", | ||||
|                 "value": 344697 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "00147dee8a7a38abbfb00dbfba365c8d6712934cc491", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7dee8a7a38abbfb00dbfba365c8d6712934cc491", | ||||
|                 "scriptpubkey_type": "v0_p2wpkh", | ||||
|                 "scriptpubkey_address": "bc1q0hhg573c4wlmqrdlhgm9ert8z2f5e3y3lf9hvx", | ||||
|                 "value": 265396 | ||||
|             } | ||||
|         ], | ||||
|         "size": 224, | ||||
|         "weight": 572, | ||||
|         "fee": 584, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "order": 2953680397, | ||||
|         "vsize": 143, | ||||
|         "adjustedVsize": 143, | ||||
|         "sigops": 5, | ||||
|         "feePerVsize": 4.083916083916084, | ||||
|         "adjustedFeePerVsize": 4.083916083916084, | ||||
|         "effectiveFeePerVsize": 4.083916083916084, | ||||
|         "firstSeen": 1691222538, | ||||
|         "uid": 526973, | ||||
|         "inputs": [ | ||||
|             526728 | ||||
|         ], | ||||
|         "position": { | ||||
|             "block": 7, | ||||
|             "vsize": 21429708.5 | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "cpfpChecked": true | ||||
|     }, | ||||
|     { | ||||
|         "txid": "8e89b20f8a7fadb0e4cdbe57a00eee224f5076bac5387fc276916724e7c4a16a", | ||||
|         "version": 2, | ||||
|         "locktime": 800571, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "35e16762459539f3a8e52c5dee6a9ccaa9e9268efed33aa2c6e1b7805e849f24", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014d4f16ef275b3e1c4a4ecbef55a164933e0f6460f", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d4f16ef275b3e1c4a4ecbef55a164933e0f6460f", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q6nckaun4k0suff8vhm6459jfx0s0v3s0ff4ukl", | ||||
|                     "value": 1528924 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "3044022019008b26e885bb43da25a11ffac147a057722072eedb68411f114f6e7eb82ebc02201b618264bb97756b88fc3bbc365b73044ac18b33b1067e31cfd5bcd0f50ed2c701", | ||||
|                     "039b71145070bd3e8af28e27fa577f2e12ab6bb4e212d3eeaef08b4bc39e8cbc13" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "67c27ed0f767526234bcd5f795a31fab8ec4d0251bf12c68f2746951f4110d90", | ||||
|                 "vout": 3, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014a7c3d613b321375054b2ac9b6114367bc034ad6f", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a7c3d613b321375054b2ac9b6114367bc034ad6f", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q5lpavyanyym4q49j4jdkz9pk00qrftt0yqzvk3", | ||||
|                     "value": 436523 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402204e67285fc656bc45ed082499b076d5dba2fa21d0d7e64a0ae52b19d69a11760002200f037d81ee540b74397844513b72b08ed92b06db76bd20b08f7a0a3b36ab13d501", | ||||
|                     "02a3ebae85f0225b6fbb5ff060afce683a4683507a57544605a29ee7d287e591b4" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "21c38fb9a2521e438c614f53b19ddd7a5594bcc4b77480e762fd4b702fad3374", | ||||
|                 "vout": 1, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "00149660e34ef88106536c816c037b5b28dd64a812e2", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 9660e34ef88106536c816c037b5b28dd64a812e2", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1qjeswxnhcsyr9xmypdsphkkegm4j2syhztgzxv4", | ||||
|                     "value": 758149 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "3044022021b556f0aa99329076bcc435338aceaf534963efcab306931b1b2b0461e16e0c02203a78942a3745c4da656bddfd8cf16b85dc04d652904e88682127cdd9ca63339001", | ||||
|                     "0298963be4a8f66aca9fcf1c6dc95547aeaa82347543190c91e094c2321142b9f0" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "aa998dbae65240a7386bf7d468459551d99c3de8e2f9057ff5f2d38e17daf788", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "00147bb7413a39943b21ded98ad5e6ad7a222d273e17", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 7bb7413a39943b21ded98ad5e6ad7a222d273e17", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q0wm5zw3ejsajrhke3t27dtt6ygkjw0sh9lltg6", | ||||
|                     "value": 1067200 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402205e2269f7d4ee0513b34354c38e920aef2dabac6f4350afb2dd105ff3ee43ae7b02202870322f2cb85cb0b2b0e38152f018bfff271dc3ec5aed0515854d0b259aaf3d01", | ||||
|                     "03b87320cf3263a644a0d3f89c1b4a7304d9dfda9eb8c891560716abcb73e88b99" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "230253d195d779d4688ba16993985cd27b2e7a687d8b889b3bc63f19ece36f20", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "001439647bd997819d12dfc72b0fb9ff9ffcb84946f8", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 39647bd997819d12dfc72b0fb9ff9ffcb84946f8", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q89j8hkvhsxw39h789v8mnlulljuyj3hc9zve97", | ||||
|                     "value": 361950 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402204f7ca868bb9b92a07fecdc6b9dd56e4e1d007ca1405952f98ed6bc416345b5f2022055320a97791417abf6628fcf6513ac5785b06c630f854d8595e96ea06c3841d301", | ||||
|                     "03a3ffe8e3ef2eea129b227e9658164bae0a6d21c17da6de9973ba34d9e04b21a0" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "670771e265a0b62dbd3c1fec2b865177eaf0bafd0ae49dd40a1c9fcd9a847a81", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014d45d1b0022c7387e42c5452ced561bdb8fd4b521", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 d45d1b0022c7387e42c5452ced561bdb8fd4b521", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q63w3kqpzcuu8usk9g5kw64smmw8afdfpxmc2m0", | ||||
|                     "value": 453275 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "3044022071312921800441903b2099e723add8702dd0f92ec11526ff87acf6967ec64cbd02203deabe7ed56d5daaa9a95c5a607b1ab705ff1c46bc6984a6dca120e63a91768601", | ||||
|                     "0257302ac8d9c4c8f9b1744f19bb432359326b9cc7bdddeeab9202749a6d92be58" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "0af82159eee2b69242f2ff032636e410b67ec1ace52e55fb0d20ed814cd64803", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "001459e4d6bfefc6b45f955a69c4aeca26348e9d54ed", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 59e4d6bfefc6b45f955a69c4aeca26348e9d54ed", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1qt8jdd0l0c669l926d8z2aj3xxj8f648dtyn7tc", | ||||
|                     "value": 439961 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "3044022027540322e92c23c5513aa2587e7feb56a8ce82f879269d6b3cbd425634b44f8e022045572dee7262b02130bfe32d8aa8abbfaa64e101abfc819bba5380c78876692d01", | ||||
|                     "03fe02262d87f4a5289d3dd66e3d9a74cd49fa1cad0249284a7451896a827249a5" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "68cf9c784870a4f888f044755f7ce318557f652461db8ef887d279672f186018", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "001454822b2d5d52597a78b630921cf439a41e32f2f9", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 54822b2d5d52597a78b630921cf439a41e32f2f9", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q2jpzkt2a2fvh579kxzfpeape5s0r9uhewhl5n4", | ||||
|                     "value": 227639 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402203ad511d6a8730748b8828bc38897d360451adf620ebdc1d229c08c097c80bef202202f50c793d95b5200cf2258e03896a3be7720df0eb3b8c810c86db74341a7e83e01", | ||||
|                     "0294992e9f4546e6e119741f908411ae531e9d1ff732d69b4dff8172aaf2a4b216" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "793f01dfdb19bf41f958fd917c16d9c4dd5d5e1a5c0434bfdb367212659d1b5b", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014f54edf8ae647b5300e2674523254e923d93d169f", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 f54edf8ae647b5300e2674523254e923d93d169f", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q748dlzhxg76nqr3xw3fry48fy0vn695lvhlkxv", | ||||
|                     "value": 227070 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402206e807ab616f4f2887ba703ae744d856142d9aca8128698419bbb67fb4fad8177022060fc65c7cd66baa88ad1e1d317a6edd5f6cb52fe8bff6e5405ffa1acf9d945d901", | ||||
|                     "02a0ad0167c6e9edf62677404d74d3b80ea276e47e758ffaa6ca17bd65ac79f7aa" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "00148a5c45ccfc29d209940d94525e2edb7743a1ad8a", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 8a5c45ccfc29d209940d94525e2edb7743a1ad8a", | ||||
|                 "scriptpubkey_type": "v0_p2wpkh", | ||||
|                 "scriptpubkey_address": "bc1q3fwytn8u98fqn9qdj3f9utkmwap6rtv2ym33zm", | ||||
|                 "value": 5500000 | ||||
|             } | ||||
|         ], | ||||
|         "size": 1375, | ||||
|         "weight": 2605, | ||||
|         "fee": 691, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "order": 1788986599, | ||||
|         "vsize": 651, | ||||
|         "adjustedVsize": 651.25, | ||||
|         "sigops": 9, | ||||
|         "feePerVsize": 1.0610364683301343, | ||||
|         "adjustedFeePerVsize": 1.0610364683301343, | ||||
|         "effectiveFeePerVsize": 1.0610364683301343, | ||||
|         "firstSeen": 1691163298, | ||||
|         "uid": 120494, | ||||
|         "inputs": [], | ||||
|         "position": { | ||||
|             "block": 7, | ||||
|             "vsize": 93780091.5 | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "cpfpChecked": true | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										121
									
								
								backend/src/__tests__/api/test-data/transactions-rbfs.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								backend/src/__tests__/api/test-data/transactions-rbfs.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,121 @@ | ||||
| [ | ||||
|     { | ||||
|         "txid": "7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6", | ||||
|         "version": 1, | ||||
|         "locktime": 0, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac", | ||||
|                     "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                     "scriptpubkey_type": "p2pkh", | ||||
|                     "scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ", | ||||
|                     "value": 799995000 | ||||
|                 }, | ||||
|                 "scriptsig": "483045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_72 3045022100aeeddfb9785c5a4b70e90d0445785c68b7a44e28853441134a70ddc4da39527602203dfe1ec1a377aaacb64ae65c7c944caf1398d2dc063f712251b4cf696d44d3cb01 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "6a4c5058325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b", | ||||
|                 "scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b8669baa9259e082f064005bc92274b559337ac317798f5d76f2d0577ed5a96042fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b", | ||||
|                 "scriptpubkey_type": "op_return", | ||||
|                 "value": 0 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87", | ||||
|                 "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL", | ||||
|                 "scriptpubkey_type": "p2sh", | ||||
|                 "scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb", | ||||
|                 "value": 155000 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2", | ||||
|                 "value": 155000 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ", | ||||
|                 "value": 799675549 | ||||
|             } | ||||
|         ], | ||||
|         "size": 350, | ||||
|         "weight": 1400, | ||||
|         "fee": 9451, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "order": 2798688215, | ||||
|         "vsize": 350, | ||||
|         "adjustedVsize": 350, | ||||
|         "sigops": 8, | ||||
|         "feePerVsize": 27.002857142857142, | ||||
|         "adjustedFeePerVsize": 27.002857142857142, | ||||
|         "effectiveFeePerVsize": 27.002857142857142, | ||||
|         "firstSeen": 1691218536, | ||||
|         "uid": 513598, | ||||
|         "inputs": [], | ||||
|         "position": { | ||||
|             "block": 0, | ||||
|             "vsize": 22166 | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "cpfpChecked": true | ||||
|     }, | ||||
|     { | ||||
|         "txid": "5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875", | ||||
|         "version": 2, | ||||
|         "locktime": 0, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz", | ||||
|                     "value": 612917 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "3045022100a0c23953ace5d022b7a6d45d1ae1730bf20a4d594bb5d4fa7aa80e4881b44d320220008f9b144805bb91995fc0f452a56e09f4ad16fa149d71ae9b5d57c742e8e2cc01", | ||||
|                     "03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 2147483649 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "0014199a98f9589364ffe5ef5bbae45ce5dfcbb873bd", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 199a98f9589364ffe5ef5bbae45ce5dfcbb873bd", | ||||
|                 "scriptpubkey_type": "v0_p2wpkh", | ||||
|                 "scriptpubkey_address": "bc1qrxdf372cjdj0le00twawgh89ml9msuaau62gk4", | ||||
|                 "value": 611909 | ||||
|             } | ||||
|         ], | ||||
|         "size": 192, | ||||
|         "weight": 438, | ||||
|         "fee": 1008, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "descendants": null, | ||||
|         "adjustedFeePerVsize": 10.2283, | ||||
|         "sigops": 1, | ||||
|         "adjustedVsize": 109.5 | ||||
|     } | ||||
| ] | ||||
							
								
								
									
										139
									
								
								backend/src/__tests__/api/test-data/transactions-replaced.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								backend/src/__tests__/api/test-data/transactions-replaced.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | ||||
| [ | ||||
|     { | ||||
|         "txid": "008592364e21c1e3d62ba9538ac78a81779897b52100af5707ab063df98964f2", | ||||
|         "version": 1, | ||||
|         "locktime": 0, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "d863deb706de5a611028f7547e16ea81d7819e44beb640fb30a9ba30c585140f", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac", | ||||
|                     "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                     "scriptpubkey_type": "p2pkh", | ||||
|                     "scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ", | ||||
|                     "value": 799995000 | ||||
|                 }, | ||||
|                 "scriptsig": "483045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601210314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_72 3045022100c1fb331d155a7d299a0451d14fa1122b328e0e239afc9ba8dc2aff449ddc5a3a02201c1e19030d1efa432f5069cd369d7ad09a67f68501345e4db35f7b799605f55601 OP_PUSHBYTES_33 0314338e3e191aea3ac9e9292611faeedf0379bbe62c30fd76c7450722a1ac47c6", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4294967293 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "6a4c5058325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b", | ||||
|                 "scriptpubkey_asm": "OP_RETURN OP_PUSHDATA1 58325b78064160b631b5a15d9078d99c0db066449fb4c59bbfa4d987ba906e2990088b2fce8c33d841b6c47a99f9597000ab04a10b34cd419fc19784d9e36f1a33fd7b000c3bce00b6000c1d1e00614b", | ||||
|                 "scriptpubkey_type": "op_return", | ||||
|                 "value": 0 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "a9144890aae025c84cb72a9730b49ca12595d6f6088d87", | ||||
|                 "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 4890aae025c84cb72a9730b49ca12595d6f6088d OP_EQUAL", | ||||
|                 "scriptpubkey_type": "p2sh", | ||||
|                 "scriptpubkey_address": "38Jht2bzmJL4EwoFvvyFzejhfEb4J7KxLb", | ||||
|                 "value": 155000 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "76a91486e7dad6617303942a448b7f8afe9653e5624a5e88ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 86e7dad6617303942a448b7f8afe9653e5624a5e OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1DJKJGApgX4W8BSQ8FRPLqX78UaCskT4r2", | ||||
|                 "value": 155000 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "76a914cd5b6566b455d043558829f6932edaae5d8f0ad388ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 cd5b6566b455d043558829f6932edaae5d8f0ad3 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1Kiq1dyVBzYLWGrBPWjChvKyzB2H95x5RJ", | ||||
|                 "value": 799676250 | ||||
|             } | ||||
|         ], | ||||
|         "size": 350, | ||||
|         "weight": 1400, | ||||
|         "fee": 8750, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "order": 4066675193, | ||||
|         "vsize": 350, | ||||
|         "adjustedVsize": 350, | ||||
|         "sigops": 8, | ||||
|         "feePerVsize": 25, | ||||
|         "adjustedFeePerVsize": 25, | ||||
|         "effectiveFeePerVsize": 25, | ||||
|         "firstSeen": 1691218516, | ||||
|         "uid": 512584, | ||||
|         "inputs": [], | ||||
|         "position": { | ||||
|             "block": 0, | ||||
|             "vsize": 13846 | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "cpfpChecked": true | ||||
|     }, | ||||
|     { | ||||
|         "txid": "b7981a624e4261c11f1246314d41e74be56af82eb557bcd054a5e0f94c023668", | ||||
|         "version": 2, | ||||
|         "locktime": 0, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "b50225a04a1d6fbbfa7a2122bc0580396f614027b3957f476229633576f06130", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014a24f913f8a9c30a4c302c2c78f2fd7addb08fd07", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 a24f913f8a9c30a4c302c2c78f2fd7addb08fd07", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1q5f8ez0u2nsc2fsczctrc7t7h4hds3lg82ewqhz", | ||||
|                     "value": 612917 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "304402204dd10f14afa41bc76d8278140ff1ec3d3f87f2c207bbb5418cc76dab30d7f6a402207877cc9c6a2c724b6ea7a1c24ac00022469f194fd1a4bd8030bbca1787d3f5f301", | ||||
|                     "03dc2c7b687019b40a68d713322675206cc266e34e5340ec982c13ff0222c3b2b6" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 2147483648 | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "76a9149d32ef812385f3811634e0c0117dd153a5de10a488ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 9d32ef812385f3811634e0c0117dd153a5de10a4 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1FLC7Bag7okAkKPCyZbgZZg3Hh1EuGZ5Rd", | ||||
|                 "value": 344697 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "00144c2671336ca8761863b4c68d64d4672491fec1b9", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 4c2671336ca8761863b4c68d64d4672491fec1b9", | ||||
|                 "scriptpubkey_type": "v0_p2wpkh", | ||||
|                 "scriptpubkey_address": "bc1qfsn8zvmv4pmpsca5c6xkf4r8yjglasdesrawcx", | ||||
|                 "value": 267636 | ||||
|             } | ||||
|         ], | ||||
|         "size": 225, | ||||
|         "weight": 573, | ||||
|         "fee": 584, | ||||
|         "status": { | ||||
|             "confirmed": false | ||||
|         }, | ||||
|         "order": 1748369996, | ||||
|         "vsize": 143, | ||||
|         "adjustedVsize": 143.25, | ||||
|         "sigops": 5, | ||||
|         "feePerVsize": 4.076788830715532, | ||||
|         "adjustedFeePerVsize": 4.076788830715532, | ||||
|         "effectiveFeePerVsize": 4.076788830715532, | ||||
|         "firstSeen": 1691222376, | ||||
|         "uid": 526515, | ||||
|         "inputs": [], | ||||
|         "position": { | ||||
|             "block": 7, | ||||
|             "vsize": 22021095.5 | ||||
|         }, | ||||
|         "bestDescendant": null, | ||||
|         "cpfpChecked": true | ||||
|     } | ||||
| ] | ||||
| @ -47,11 +47,17 @@ describe('Mempool Backend Config', () => { | ||||
|         DISK_CACHE_BLOCK_INTERVAL: 6, | ||||
|         MAX_PUSH_TX_SIZE_WEIGHT: 400000, | ||||
|         ALLOW_UNREACHABLE: true, | ||||
|         PRICE_UPDATES_PER_HOUR: 1, | ||||
|       }); | ||||
| 
 | ||||
|       expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); | ||||
| 
 | ||||
|       expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 }); | ||||
|       expect(config.ESPLORA).toStrictEqual({ | ||||
|         REST_API_URL: 'http://127.0.0.1:3000', | ||||
|         UNIX_SOCKET_PATH: null, | ||||
|         RETRY_UNIX_SOCKET_AFTER: 30000, | ||||
|         FALLBACK: [], | ||||
|        }); | ||||
| 
 | ||||
|       expect(config.CORE_RPC).toStrictEqual({ | ||||
|         HOST: '127.0.0.1', | ||||
| @ -101,11 +107,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 +130,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 +169,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); | ||||
|     }); | ||||
|   }); | ||||
| @ -180,41 +186,50 @@ describe('Mempool Backend Config', () => { | ||||
|         for (const [key, value] of Object.entries(jsonObj)) { | ||||
|           // We have a few cases where we can't follow the pattern
 | ||||
|           if (root === 'MEMPOOL' && key === 'HTTP_PORT') { | ||||
|             console.log('skipping check for MEMPOOL_HTTP_PORT'); | ||||
|             if (process.env.CI) { | ||||
|               console.log('skipping check for MEMPOOL_HTTP_PORT'); | ||||
|             } | ||||
|             continue; | ||||
|           } | ||||
|           switch (typeof value) { | ||||
|             case 'object': { | ||||
|               if (Array.isArray(value)) { | ||||
|                 continue; | ||||
|               } else { | ||||
|                 parseJson(value, key); | ||||
|               } | ||||
|               break; | ||||
|             } | ||||
|             default: { | ||||
| 
 | ||||
|           if (root) { | ||||
|               //The flattened string, i.e, __MEMPOOL_ENABLED__
 | ||||
|               const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`; | ||||
| 
 | ||||
|               //The string used as the environment variable, i.e, MEMPOOL_ENABLED
 | ||||
|               const envVarStr = `${root ? root : ''}_${key}`; | ||||
| 
 | ||||
|               let defaultEntry; | ||||
|               //The string used as the default value, to be checked as a regex, i.e, __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=(.*)}
 | ||||
|               const defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}'; | ||||
| 
 | ||||
|               console.log(`looking for ${defaultEntry} in the start.sh script`); | ||||
|               const re = new RegExp(defaultEntry); | ||||
|               expect(startSh).toMatch(re); | ||||
|               if (Array.isArray(value)) { | ||||
|                 defaultEntry = `${replaceStr}=\${${envVarStr}:=[]}`; | ||||
|                 if (process.env.CI) { | ||||
|                   console.log(`looking for ${defaultEntry} in the start.sh script`); | ||||
|                 } | ||||
|                 //Regex matching does not work with the array values
 | ||||
|                 expect(startSh).toContain(defaultEntry); | ||||
|               } else { | ||||
|                  defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}'; | ||||
|                  if (process.env.CI) { | ||||
|                   console.log(`looking for ${defaultEntry} in the start.sh script`); | ||||
|                 } | ||||
|                 const re = new RegExp(defaultEntry); | ||||
|                 expect(startSh).toMatch(re); | ||||
|               } | ||||
| 
 | ||||
|               //The string that actually replaces the values in the config file
 | ||||
|               const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json'; | ||||
|               console.log(`looking for ${sedStr} in the start.sh script`); | ||||
|               if (process.env.CI) { | ||||
|                 console.log(`looking for ${sedStr} in the start.sh script`); | ||||
|               } | ||||
|               expect(startSh).toContain(sedStr); | ||||
|               break; | ||||
|             } | ||||
|           else { | ||||
|             parseJson(value, key); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       parseJson(fixture); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @ -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>; | ||||
| @ -22,6 +23,8 @@ export interface AbstractBitcoinApi { | ||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; | ||||
|   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; | ||||
|   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; | ||||
| 
 | ||||
|   startHealthChecks(): void; | ||||
| } | ||||
| export interface BitcoinRpcCredentials { | ||||
|   host: string; | ||||
|  | ||||
| @ -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> { | ||||
| @ -350,6 +355,7 @@ class BitcoinApi implements AbstractBitcoinApi { | ||||
|     return transaction; | ||||
|   } | ||||
| 
 | ||||
|   public startHealthChecks(): void {}; | ||||
| } | ||||
| 
 | ||||
| export default BitcoinApi; | ||||
|  | ||||
| @ -214,6 +214,7 @@ class BitcoinRoutes { | ||||
|           effectiveFeePerVsize: tx.effectiveFeePerVsize || null, | ||||
|           sigops: tx.sigops, | ||||
|           adjustedVsize: tx.adjustedVsize, | ||||
|           acceleration: tx.acceleration | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
| @ -1,112 +1,260 @@ | ||||
| import config from '../../config'; | ||||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| import axios, { AxiosResponse } from 'axios'; | ||||
| import http from 'http'; | ||||
| import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; | ||||
| import { IEsploraApi } from './esplora-api.interface'; | ||||
| import logger from '../../logger'; | ||||
| 
 | ||||
| const axiosConnection = axios.create({ | ||||
|   httpAgent: new http.Agent({ keepAlive: true, }) | ||||
| }); | ||||
| interface FailoverHost { | ||||
|   host: string, | ||||
|   rtts: number[], | ||||
|   rtt: number | ||||
|   failures: number, | ||||
|   socket?: boolean, | ||||
|   outOfSync?: boolean, | ||||
|   unreachable?: boolean, | ||||
|   preferred?: boolean, | ||||
| } | ||||
| 
 | ||||
| class ElectrsApi implements AbstractBitcoinApi { | ||||
|   private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? { | ||||
|     socketPath: config.ESPLORA.UNIX_SOCKET_PATH, | ||||
|     timeout: 10000, | ||||
|   } : { | ||||
|     timeout: 10000, | ||||
|   }; | ||||
|   private axiosConfigTcpSocketOnly: AxiosRequestConfig = { | ||||
|     timeout: 10000, | ||||
|   }; | ||||
| 
 | ||||
|   unixSocketRetryTimeout; | ||||
|   activeAxiosConfig; | ||||
| class FailoverRouter { | ||||
|   activeHost: FailoverHost; | ||||
|   fallbackHost: FailoverHost; | ||||
|   hosts: FailoverHost[]; | ||||
|   multihost: boolean; | ||||
|   pollInterval: number = 60000; | ||||
|   pollTimer: NodeJS.Timeout | null = null; | ||||
|   pollConnection = axios.create(); | ||||
|   requestConnection = axios.create({ | ||||
|     httpAgent: new http.Agent({ keepAlive: true }) | ||||
|   }); | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.activeAxiosConfig = this.axiosConfigWithUnixSocket; | ||||
|     // setup list of hosts
 | ||||
|     this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { | ||||
|       return { | ||||
|         host: domain, | ||||
|         rtts: [], | ||||
|         rtt: Infinity, | ||||
|         failures: 0, | ||||
|       }; | ||||
|     }); | ||||
|     this.activeHost = { | ||||
|       host: config.ESPLORA.UNIX_SOCKET_PATH || config.ESPLORA.REST_API_URL, | ||||
|       rtts: [], | ||||
|       rtt: 0, | ||||
|       failures: 0, | ||||
|       socket: !!config.ESPLORA.UNIX_SOCKET_PATH, | ||||
|       preferred: true, | ||||
|     }; | ||||
|     this.fallbackHost = this.activeHost; | ||||
|     this.hosts.unshift(this.activeHost); | ||||
|     this.multihost = this.hosts.length > 1; | ||||
|   } | ||||
| 
 | ||||
|   fallbackToTcpSocket() { | ||||
|     if (!this.unixSocketRetryTimeout) { | ||||
|       logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`); | ||||
|       // Retry the unix socket after a few seconds
 | ||||
|       this.unixSocketRetryTimeout = setTimeout(() => { | ||||
|         logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`); | ||||
|         this.activeAxiosConfig = this.axiosConfigWithUnixSocket; | ||||
|         this.unixSocketRetryTimeout = undefined; | ||||
|       }, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER); | ||||
|   public startHealthChecks(): void { | ||||
|     // use axios interceptors to measure request rtt
 | ||||
|     this.pollConnection.interceptors.request.use((config) => { | ||||
|       config['meta'] = { startTime: Date.now() }; | ||||
|       return config; | ||||
|     }); | ||||
|     this.pollConnection.interceptors.response.use((response) => { | ||||
|       response.config['meta'].rtt = Date.now() - response.config['meta'].startTime; | ||||
|       return response; | ||||
|     }); | ||||
| 
 | ||||
|     if (this.multihost) { | ||||
|       this.pollHosts(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // start polling hosts to measure availability & rtt
 | ||||
|   private async pollHosts(): Promise<void> { | ||||
|     if (this.pollTimer) { | ||||
|       clearTimeout(this.pollTimer); | ||||
|     } | ||||
| 
 | ||||
|     // Use the TCP socket (reach a different esplora instance through nginx)
 | ||||
|     this.activeAxiosConfig = this.axiosConfigTcpSocketOnly; | ||||
|     const results = await Promise.allSettled(this.hosts.map(async (host) => { | ||||
|       if (host.socket) { | ||||
|         return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 5000 }); | ||||
|       } else { | ||||
|         return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 5000 }); | ||||
|       } | ||||
|     })); | ||||
|     const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0); | ||||
| 
 | ||||
|     // update rtts & sync status
 | ||||
|     for (let i = 0; i < results.length; i++) { | ||||
|       const host = this.hosts[i]; | ||||
|       const result = results[i].status === 'fulfilled' ? (results[i] as PromiseFulfilledResult<AxiosResponse<number, any>>).value : null; | ||||
|       if (result) { | ||||
|         const height = result.data; | ||||
|         const rtt = result.config['meta'].rtt; | ||||
|         host.rtts.unshift(rtt); | ||||
|         host.rtts.slice(0, 5); | ||||
|         host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; | ||||
|         if (height == null || isNaN(height) || (maxHeight - height > 2)) { | ||||
|           host.outOfSync = true; | ||||
|         } else { | ||||
|           host.outOfSync = false; | ||||
|         } | ||||
|         host.unreachable = false; | ||||
|       } else { | ||||
|         host.unreachable = true; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.sortHosts(); | ||||
| 
 | ||||
|     logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`); | ||||
| 
 | ||||
|     // switch if the current host is out of sync or significantly slower than the next best alternative
 | ||||
|     if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) { | ||||
|       if (this.activeHost.unreachable) { | ||||
|         logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`); | ||||
|       } else if (this.activeHost.outOfSync) { | ||||
|         logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`); | ||||
|       } else { | ||||
|         logger.debug(`${this.activeHost.host} is no longer the best esplora host`); | ||||
|       } | ||||
|       this.electHost(); | ||||
|     } | ||||
| 
 | ||||
|     this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval); | ||||
|   } | ||||
| 
 | ||||
|   $queryWrapper<T>(url, responseType = 'json'): Promise<T> { | ||||
|     return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType }) | ||||
|       .then((response) => response.data) | ||||
|   // sort hosts by connection quality, and update default fallback
 | ||||
|   private sortHosts(): void { | ||||
|     // sort by connection quality
 | ||||
|     this.hosts.sort((a, b) => { | ||||
|       if ((a.unreachable || a.outOfSync) === (b.unreachable || b.outOfSync)) { | ||||
|         if  (a.preferred === b.preferred) { | ||||
|           // lower rtt is best
 | ||||
|           return a.rtt - b.rtt; | ||||
|         } else { // unless we have a preferred host
 | ||||
|           return a.preferred ? -1 : 1; | ||||
|         } | ||||
|       } else { // or the host is out of sync
 | ||||
|         return (a.unreachable || a.outOfSync) ? 1 : -1; | ||||
|       } | ||||
|     }); | ||||
|     if (this.hosts.length > 1 && this.hosts[0] === this.activeHost) { | ||||
|       this.fallbackHost = this.hosts[1]; | ||||
|     } else { | ||||
|       this.fallbackHost = this.hosts[0]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // depose the active host and choose the next best replacement
 | ||||
|   private electHost(): void { | ||||
|     this.activeHost.outOfSync = true; | ||||
|     this.activeHost.failures = 0; | ||||
|     this.sortHosts(); | ||||
|     this.activeHost = this.hosts[0]; | ||||
|     logger.warn(`Switching esplora host to ${this.activeHost.host}`); | ||||
|   } | ||||
| 
 | ||||
|   private addFailure(host: FailoverHost): FailoverHost { | ||||
|     host.failures++; | ||||
|     if (host.failures > 5 && this.multihost) { | ||||
|       logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`); | ||||
|       this.electHost(); | ||||
|       return this.activeHost; | ||||
|     } else { | ||||
|       return this.fallbackHost; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> { | ||||
|     let axiosConfig; | ||||
|     let url; | ||||
|     if (host.socket) { | ||||
|       axiosConfig = { socketPath: host.host, timeout: 10000, responseType }; | ||||
|       url = path; | ||||
|     } else { | ||||
|       axiosConfig = { timeout: 10000, responseType }; | ||||
|       url = host.host + path; | ||||
|     } | ||||
|     return (method === 'post' | ||||
|         ? this.requestConnection.post<T>(url, data, axiosConfig) | ||||
|         : this.requestConnection.get<T>(url, axiosConfig) | ||||
|     ).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; }) | ||||
|       .catch((e) => { | ||||
|         if (e?.code === 'ECONNREFUSED') { | ||||
|           this.fallbackToTcpSocket(); | ||||
|         let fallbackHost = this.fallbackHost; | ||||
|         if (e?.response?.status !== 404) { | ||||
|           logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`); | ||||
|           fallbackHost = this.addFailure(host); | ||||
|         } | ||||
|         if (retry && e?.code === 'ECONNREFUSED' && this.multihost) { | ||||
|           // Retry immediately
 | ||||
|           return axiosConnection.get<T>(url, this.activeAxiosConfig) | ||||
|             .then((response) => response.data) | ||||
|             .catch((e) => { | ||||
|               logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`); | ||||
|               throw e; | ||||
|             }); | ||||
|           return this.$query(method, path, data, responseType, fallbackHost, false); | ||||
|         } else { | ||||
|           throw e; | ||||
|         } | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   public async $get<T>(path, responseType = 'json'): Promise<T> { | ||||
|     return this.$query<T>('get', path, null, responseType); | ||||
|   } | ||||
| 
 | ||||
|   public async $post<T>(path, data: any, responseType = 'json'): Promise<T> { | ||||
|     return this.$query<T>('post', path, data, responseType); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ElectrsApi implements AbstractBitcoinApi { | ||||
|   private failoverRouter = new FailoverRouter(); | ||||
| 
 | ||||
|   $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { | ||||
|     return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids'); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids'); | ||||
|   } | ||||
| 
 | ||||
|   $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> { | ||||
|     return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txId); | ||||
|   } | ||||
| 
 | ||||
|   async $getMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> { | ||||
|     return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); | ||||
|   async $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]> { | ||||
|     return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/mempool/txs', txids, 'json'); | ||||
|   } | ||||
| 
 | ||||
|   async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> { | ||||
|     return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); | ||||
|   } | ||||
| 
 | ||||
|   $getTransactionHex(txId: string): Promise<string> { | ||||
|     return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex'); | ||||
|     return this.failoverRouter.$get<string>('/tx/' + txId + '/hex'); | ||||
|   } | ||||
| 
 | ||||
|   $getBlockHeightTip(): Promise<number> { | ||||
|     return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height'); | ||||
|     return this.failoverRouter.$get<number>('/blocks/tip/height'); | ||||
|   } | ||||
| 
 | ||||
|   $getBlockHashTip(): Promise<string> { | ||||
|     return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash'); | ||||
|     return this.failoverRouter.$get<string>('/blocks/tip/hash'); | ||||
|   } | ||||
| 
 | ||||
|   $getTxIdsForBlock(hash: string): Promise<string[]> { | ||||
|     return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); | ||||
|     return this.failoverRouter.$get<string[]>('/block/' + hash + '/txids'); | ||||
|   } | ||||
| 
 | ||||
|   $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> { | ||||
|     return this.$queryWrapper<IEsploraApi.Transaction[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs'); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/block/' + hash + '/txs'); | ||||
|   } | ||||
| 
 | ||||
|   $getBlockHash(height: number): Promise<string> { | ||||
|     return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height); | ||||
|     return this.failoverRouter.$get<string>('/block-height/' + height); | ||||
|   } | ||||
| 
 | ||||
|   $getBlockHeader(hash: string): Promise<string> { | ||||
|     return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header'); | ||||
|     return this.failoverRouter.$get<string>('/block/' + hash + '/header'); | ||||
|   } | ||||
| 
 | ||||
|   $getBlock(hash: string): Promise<IEsploraApi.Block> { | ||||
|     return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Block>('/block/' + hash); | ||||
|   } | ||||
| 
 | ||||
|   $getRawBlock(hash: string): Promise<Buffer> { | ||||
|     return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer') | ||||
|     return this.failoverRouter.$get<any>('/block/' + hash + '/raw', 'arraybuffer') | ||||
|       .then((response) => { return Buffer.from(response.data); }); | ||||
|   } | ||||
| 
 | ||||
| @ -135,11 +283,11 @@ class ElectrsApi implements AbstractBitcoinApi { | ||||
|   } | ||||
| 
 | ||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { | ||||
|     return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); | ||||
|   } | ||||
| 
 | ||||
|   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> { | ||||
|     return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends'); | ||||
|     return this.failoverRouter.$get<IEsploraApi.Outspend[]>('/tx/' + txId + '/outspends'); | ||||
|   } | ||||
| 
 | ||||
|   async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> { | ||||
| @ -150,6 +298,10 @@ class ElectrsApi implements AbstractBitcoinApi { | ||||
|     } | ||||
|     return outspends; | ||||
|   } | ||||
| 
 | ||||
|   public startHealthChecks(): void { | ||||
|     this.failoverRouter.startHealthChecks(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default ElectrsApi; | ||||
|  | ||||
| @ -674,7 +674,11 @@ class Blocks { | ||||
|           this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment'); | ||||
|           const previousPeriodBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(previousPeriodBlockHash); | ||||
|           this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment'); | ||||
|           this.previousDifficultyRetarget = calcBitsDifference(previousPeriodBlock.bits, block.bits); | ||||
|           if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { | ||||
|             this.previousDifficultyRetarget = NaN; | ||||
|           } else { | ||||
|             this.previousDifficultyRetarget = calcBitsDifference(previousPeriodBlock.bits, block.bits); | ||||
|           } | ||||
|           logger.debug(`Initial difficulty adjustment data set.`); | ||||
|         } | ||||
|       } else { | ||||
| @ -783,20 +787,31 @@ class Blocks { | ||||
| 
 | ||||
|       if (block.height % 2016 === 0) { | ||||
|         if (Common.indexingEnabled()) { | ||||
|           let adjustment; | ||||
|           if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { | ||||
|             adjustment = NaN; | ||||
|           } else { | ||||
|             adjustment = Math.round( | ||||
|               // calcBitsDifference returns +- percentage, +100 returns to positive, /100 returns to ratio.
 | ||||
|               // Instead of actually doing /100, just reduce the multiplier.
 | ||||
|               (calcBitsDifference(this.currentBits, block.bits) + 100) * 10000 | ||||
|             ) / 1000000; // Remove float point noise
 | ||||
|           } | ||||
| 
 | ||||
|           await DifficultyAdjustmentsRepository.$saveAdjustments({ | ||||
|             time: block.timestamp, | ||||
|             height: block.height, | ||||
|             difficulty: block.difficulty, | ||||
|             adjustment: Math.round( | ||||
|               // calcBitsDifference returns +- percentage, +100 returns to positive, /100 returns to ratio.
 | ||||
|               // Instead of actually doing /100, just reduce the multiplier.
 | ||||
|               (calcBitsDifference(this.currentBits, block.bits) + 100) * 10000 | ||||
|             ) / 1000000, // Remove float point noise
 | ||||
|             adjustment, | ||||
|           }); | ||||
|           this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`); | ||||
|         } | ||||
| 
 | ||||
|         this.previousDifficultyRetarget = calcBitsDifference(this.currentBits, block.bits); | ||||
|         if (['liquid', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { | ||||
|           this.previousDifficultyRetarget = NaN; | ||||
|         } else { | ||||
|           this.previousDifficultyRetarget = calcBitsDifference(this.currentBits, block.bits); | ||||
|         } | ||||
|         this.lastDifficultyAdjustmentTime = block.timestamp; | ||||
|         this.currentBits = block.bits; | ||||
|       } | ||||
|  | ||||
| @ -59,10 +59,12 @@ export class Common { | ||||
|     return arr; | ||||
|   } | ||||
| 
 | ||||
|   static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended[] } { | ||||
|   static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { | ||||
|     const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; | ||||
|     added | ||||
|       .forEach((addedTx) => { | ||||
| 
 | ||||
|     // For small N, a naive nested loop is extremely fast, but it doesn't scale
 | ||||
|     if (added.length < 1000 && deleted.length < 50 && !forceScalable) { | ||||
|       added.forEach((addedTx) => { | ||||
|         const foundMatches = deleted.filter((deletedTx) => { | ||||
|           // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | ||||
|           return addedTx.fee > deletedTx.fee | ||||
| @ -73,9 +75,40 @@ export class Common { | ||||
|               addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); | ||||
|             }); | ||||
|         if (foundMatches?.length) { | ||||
|           matches[addedTx.txid] = foundMatches; | ||||
|           matches[addedTx.txid] = [...new Set(foundMatches)]; | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       // for large N, build a lookup table of prevouts we can check in ~constant time
 | ||||
|       const deletedSpendMap: { [txid: string]: { [vout: number]: MempoolTransactionExtended } } = {}; | ||||
|       for (const tx of deleted) { | ||||
|         for (const vin of tx.vin) { | ||||
|           if (!deletedSpendMap[vin.txid]) { | ||||
|             deletedSpendMap[vin.txid] = {}; | ||||
|           } | ||||
|           deletedSpendMap[vin.txid][vin.vout] = tx; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       for (const addedTx of added) { | ||||
|         const foundMatches = new Set<MempoolTransactionExtended>(); | ||||
|         for (const vin of addedTx.vin) { | ||||
|           const deletedTx = deletedSpendMap[vin.txid]?.[vin.vout]; | ||||
|           if (deletedTx && deletedTx.txid !== addedTx.txid | ||||
|               // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
 | ||||
|               && addedTx.fee > deletedTx.fee | ||||
|               // The new transaction must pay more fee per kB than the replaced tx.
 | ||||
|               && addedTx.adjustedFeePerVsize > deletedTx.adjustedFeePerVsize | ||||
|           ) { | ||||
|             foundMatches.add(deletedTx); | ||||
|           } | ||||
|           if (foundMatches.size) { | ||||
|             matches[addedTx.txid] = [...foundMatches]; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return matches; | ||||
|   } | ||||
| 
 | ||||
| @ -111,6 +144,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 +494,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); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
| @ -32,13 +32,13 @@ export interface DifficultyAdjustment { | ||||
| export function calcBitsDifference(oldBits: number, newBits: number): number { | ||||
|   // Must be
 | ||||
|   // - integer
 | ||||
|   // - highest exponent is 0x1f, so max value (as integer) is 0x1f0000ff
 | ||||
|   // - highest exponent is 0x20, so max value (as integer) is 0x207fffff
 | ||||
|   // - min value is 1 (exponent = 0)
 | ||||
|   // - highest bit of the number-part is +- sign, it must not be 1
 | ||||
|   const verifyBits = (bits: number): void => { | ||||
|     if ( | ||||
|       Math.floor(bits) !== bits || | ||||
|       bits > 0x1f0000ff || | ||||
|       bits > 0x207fffff || | ||||
|       bits < 1 || | ||||
|       (bits & 0x00800000) !== 0 || | ||||
|       (bits & 0x007fffff) === 0 | ||||
|  | ||||
| @ -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
 | ||||
|     this.resetUids(); | ||||
|     for (const tx of Object.values(newMempool)) { | ||||
|       this.setUid(tx); | ||||
|     if (saveResults) { | ||||
|       this.resetUids(); | ||||
|     } | ||||
|     // 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,19 +436,22 @@ 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; | ||||
|       } | ||||
|       this.removeUids(removedUids); | ||||
|       logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`); | ||||
|     } catch (e) { | ||||
|       logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); | ||||
|       this.resetRustGbt(); | ||||
|       return this.mempoolBlocks; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], 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].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize); | ||||
|         mempool[txid].effectiveFeePerVsize = rate; | ||||
|         mempool[txid].cpfpChecked = false; | ||||
|       } | ||||
| @ -463,11 +495,16 @@ class MempoolBlocks { | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|           if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { | ||||
|             mempoolTx.cpfpDirty = true; | ||||
|           } | ||||
|           Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     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 +533,26 @@ class MempoolBlocks { | ||||
|             mempoolTx.cpfpChecked = true; | ||||
|           } | ||||
| 
 | ||||
|           const acceleration = accelerations[txid]; | ||||
|           if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { | ||||
|             if (!mempoolTx.acceleration) { | ||||
|               mempoolTx.cpfpDirty = true; | ||||
|             } | ||||
|             mempoolTx.acceleration = true; | ||||
|             for (const ancestor of mempoolTx.ancestors || []) { | ||||
|               if (!mempool[ancestor.txid].acceleration) { | ||||
|                 mempool[ancestor.txid].cpfpDirty = true; | ||||
|               } | ||||
|               mempool[ancestor.txid].acceleration = true; | ||||
|               isAccelerated[ancestor.txid] = true; | ||||
|             } | ||||
|           } else { | ||||
|             if (mempoolTx.acceleration) { | ||||
|               mempoolTx.cpfpDirty = true; | ||||
|             } | ||||
|             delete mempoolTx.acceleration; | ||||
|           } | ||||
| 
 | ||||
|           // online calculation of stack-of-blocks fee stats
 | ||||
|           if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) { | ||||
|             feeStatsCalculator.processNext(mempoolTx); | ||||
| @ -532,7 +589,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,31 +234,37 @@ 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; | ||||
|             if (this.inSync) { | ||||
|               this.txPerSecondArray.push(new Date().getTime()); | ||||
|               this.vBytesPerSecondArray.push({ | ||||
|                 unixTime: new Date().getTime(), | ||||
|                 vSize: transaction.vsize, | ||||
|               }); | ||||
|             } | ||||
|             hasChange = true; | ||||
|             newTransactions.push(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'); | ||||
| 
 | ||||
|             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)); | ||||
|         for (const transaction of txs) { | ||||
|           this.mempoolCache[transaction.txid] = transaction; | ||||
|           if (this.inSync) { | ||||
|             this.txPerSecondArray.push(new Date().getTime()); | ||||
|             this.vBytesPerSecondArray.push({ | ||||
|               unixTime: new Date().getTime(), | ||||
|               vSize: transaction.vsize, | ||||
|             }); | ||||
|           } | ||||
|           hasChange = true; | ||||
|           newTransactions.push(transaction); | ||||
| 
 | ||||
|           if (config.REDIS.ENABLED) { | ||||
|             await redisCache.$addTransaction(transaction); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (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(), | ||||
|  | ||||
| @ -12,6 +12,7 @@ import PricesRepository from '../../repositories/PricesRepository'; | ||||
| class MiningRoutes { | ||||
|   public initRoutes(app: Application) { | ||||
|     app | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools', this.$listPools) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', this.$getPools) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', this.$getPoolHistoricalHashrate) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', this.$getPoolBlocks) | ||||
| @ -41,6 +42,10 @@ class MiningRoutes { | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); | ||||
|       if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { | ||||
|         res.status(400).send('Prices are not available on testnets.'); | ||||
|         return; | ||||
|       } | ||||
|       if (req.query.timestamp) { | ||||
|         res.status(200).send(await PricesRepository.$getNearestHistoricalPrice( | ||||
|           parseInt(<string>req.query.timestamp ?? 0, 10) | ||||
| @ -88,6 +93,29 @@ class MiningRoutes { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $listPools(req: Request, res: Response): Promise<void> { | ||||
|     try { | ||||
|       res.header('Pragma', 'public'); | ||||
|       res.header('Cache-control', 'public'); | ||||
|       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||
| 
 | ||||
|       const pools = await mining.$listPools(); | ||||
|       if (!pools) { | ||||
|         res.status(500).end(); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       res.header('X-total-count', pools.length.toString()); | ||||
|       if (pools.length === 0) { | ||||
|         res.status(204).send(); | ||||
|       } else { | ||||
|         res.json(pools); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getPools(req: Request, res: Response) { | ||||
|     try { | ||||
|       const stats = await mining.$getPoolsStats(req.params.interval); | ||||
|  | ||||
| @ -26,7 +26,7 @@ class Mining { | ||||
|   /** | ||||
|    * Get historical blocks health | ||||
|    */ | ||||
|    public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> { | ||||
|   public async $getBlocksHealthHistory(interval: string | null = null): Promise<any> { | ||||
|     return await BlocksAuditsRepository.$getBlocksHealthHistory( | ||||
|       this.getTimeRange(interval), | ||||
|       Common.getSqlInterval(interval) | ||||
| @ -56,7 +56,7 @@ class Mining { | ||||
|   /** | ||||
|    * Get historical block fee rates percentiles | ||||
|    */ | ||||
|    public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> { | ||||
|   public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> { | ||||
|     return await BlocksRepository.$getHistoricalBlockFeeRates( | ||||
|       this.getTimeRange(interval), | ||||
|       Common.getSqlInterval(interval) | ||||
| @ -66,7 +66,7 @@ class Mining { | ||||
|   /** | ||||
|    * Get historical block sizes | ||||
|    */ | ||||
|    public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> { | ||||
|   public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> { | ||||
|     return await BlocksRepository.$getHistoricalBlockSizes( | ||||
|       this.getTimeRange(interval), | ||||
|       Common.getSqlInterval(interval) | ||||
| @ -76,7 +76,7 @@ class Mining { | ||||
|   /** | ||||
|    * Get historical block weights | ||||
|    */ | ||||
|    public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> { | ||||
|   public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> { | ||||
|     return await BlocksRepository.$getHistoricalBlockWeights( | ||||
|       this.getTimeRange(interval), | ||||
|       Common.getSqlInterval(interval) | ||||
| @ -107,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); | ||||
|     }); | ||||
| @ -594,6 +595,20 @@ class Mining { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * List existing mining pools | ||||
|    */ | ||||
|   public async $listPools(): Promise<{name: string, slug: string, unique_id: number}[] | null> { | ||||
|     const [rows] = await database.query(` | ||||
|       SELECT | ||||
|         name, | ||||
|         slug, | ||||
|         unique_id | ||||
|       FROM pools` | ||||
|     ); | ||||
|     return rows as {name: string, slug: string, unique_id: number}[]; | ||||
|   } | ||||
| 
 | ||||
|   private getDateMidnight(date: Date): Date { | ||||
|     date.setUTCHours(0); | ||||
|     date.setUTCMinutes(0); | ||||
|  | ||||
							
								
								
									
										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,8 @@ 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'; | ||||
| import pLimit from '../utils/p-limit'; | ||||
| 
 | ||||
| class TransactionUtils { | ||||
|   constructor() { } | ||||
| @ -71,6 +73,28 @@ 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 limiter = pLimit(8); // Run 8 requests at a time
 | ||||
|       const results = await Promise.allSettled(txids.map( | ||||
|         txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore)) | ||||
|       )); | ||||
|       return results.filter(reply => reply.status === 'fulfilled') | ||||
|         .map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).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 { | ||||
| @ -190,18 +198,14 @@ class WebsocketHandler { | ||||
|                 matchedAddress = matchedAddress.toLowerCase(); | ||||
|               } | ||||
|               if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) { | ||||
|                 client['track-address'] = null; | ||||
|                 client['track-scriptpubkey'] = '41' + matchedAddress + 'ac'; | ||||
|               } else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) { | ||||
|                 client['track-address'] = null; | ||||
|                 client['track-scriptpubkey'] = '21' + matchedAddress + 'ac'; | ||||
|                 client['track-address'] = '41' + matchedAddress + 'ac'; | ||||
|               } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) { | ||||
|                 client['track-address'] = '21' + matchedAddress + 'ac'; | ||||
|               } else { | ||||
|                 client['track-address'] = matchedAddress; | ||||
|                 client['track-scriptpubkey'] = null; | ||||
|               } | ||||
|             } else { | ||||
|               client['track-address'] = null; | ||||
|               client['track-scriptpubkey'] = null; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
| @ -390,7 +394,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 +403,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); | ||||
| @ -480,6 +484,9 @@ class WebsocketHandler { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // pre-compute address transactions
 | ||||
|     const addressCache = this.makeAddressCache(newTransactions); | ||||
| 
 | ||||
|     this.wss.clients.forEach(async (client) => { | ||||
|       if (client.readyState !== WebSocket.OPEN) { | ||||
|         return; | ||||
| @ -519,78 +526,13 @@ class WebsocketHandler { | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-address']) { | ||||
|         const foundTransactions: TransactionExtended[] = []; | ||||
|         const foundTransactions = Array.from(addressCache[client['track-address']]?.values() || []); | ||||
|         // txs may be missing prevouts in non-esplora backends
 | ||||
|         // so fetch the full transactions now
 | ||||
|         const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions; | ||||
| 
 | ||||
|         for (const tx of newTransactions) { | ||||
|           const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']); | ||||
|           if (someVin) { | ||||
|             if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|               try { | ||||
|                 const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); | ||||
|                 foundTransactions.push(fullTx); | ||||
|               } catch (e) { | ||||
|                 logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||
|               } | ||||
|             } else { | ||||
|               foundTransactions.push(tx); | ||||
|             } | ||||
|             return; | ||||
|           } | ||||
|           const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']); | ||||
|           if (someVout) { | ||||
|             if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|               try { | ||||
|                 const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); | ||||
|                 foundTransactions.push(fullTx); | ||||
|               } catch (e) { | ||||
|                 logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||
|               } | ||||
|             } else { | ||||
|               foundTransactions.push(tx); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (foundTransactions.length) { | ||||
|           response['address-transactions'] = JSON.stringify(foundTransactions); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-scriptpubkey']) { | ||||
|         const foundTransactions: TransactionExtended[] = []; | ||||
| 
 | ||||
|         for (const tx of newTransactions) { | ||||
|           const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']); | ||||
|           if (someVin) { | ||||
|             if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|               try { | ||||
|                 const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); | ||||
|                 foundTransactions.push(fullTx); | ||||
|               } catch (e) { | ||||
|                 logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||
|               } | ||||
|             } else { | ||||
|               foundTransactions.push(tx); | ||||
|             } | ||||
|             return; | ||||
|           } | ||||
|           const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']); | ||||
|           if (someVout) { | ||||
|             if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||
|               try { | ||||
|                 const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); | ||||
|                 foundTransactions.push(fullTx); | ||||
|               } catch (e) { | ||||
|                 logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||
|               } | ||||
|             } else { | ||||
|               foundTransactions.push(tx); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (foundTransactions.length) { | ||||
|           response['address-transactions'] = JSON.stringify(foundTransactions); | ||||
|         if (fullTransactions.length) { | ||||
|           response['address-transactions'] = JSON.stringify(fullTransactions); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
| @ -598,7 +540,6 @@ class WebsocketHandler { | ||||
|         const foundTransactions: TransactionExtended[] = []; | ||||
| 
 | ||||
|         newTransactions.forEach((tx) => { | ||||
| 
 | ||||
|           if (client['track-asset'] === Common.nativeAssetId) { | ||||
|             if (tx.vin.some((vin) => !!vin.is_pegin)) { | ||||
|               foundTransactions.push(tx); | ||||
| @ -645,10 +586,25 @@ class WebsocketHandler { | ||||
| 
 | ||||
|         const mempoolTx = newMempool[trackTxid]; | ||||
|         if (mempoolTx && mempoolTx.position) { | ||||
|           response['txPosition'] = JSON.stringify({ | ||||
|           const positionData = { | ||||
|             txid: trackTxid, | ||||
|             position: mempoolTx.position, | ||||
|           }); | ||||
|             position: { | ||||
|               ...mempoolTx.position, | ||||
|               accelerated: mempoolTx.acceleration || undefined, | ||||
|             } | ||||
|           }; | ||||
|           if (mempoolTx.cpfpDirty) { | ||||
|             positionData['cpfp'] = { | ||||
|               ancestors: mempoolTx.ancestors, | ||||
|               bestDescendant: mempoolTx.bestDescendant || null, | ||||
|               descendants: mempoolTx.descendants || null, | ||||
|               effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null, | ||||
|               sigops: mempoolTx.sigops, | ||||
|               adjustedVsize: mempoolTx.adjustedVsize, | ||||
|               acceleration: mempoolTx.acceleration | ||||
|             }; | ||||
|           } | ||||
|           response['txPosition'] = JSON.stringify(positionData); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
| @ -695,6 +651,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 +659,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 { | ||||
|         projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||
|         if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) { | ||||
|           if (config.MEMPOOL.RUST_GBT) { | ||||
|             projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id); | ||||
|           } else { | ||||
|             projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id); | ||||
|           } | ||||
|         } else { | ||||
|           projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (Common.indexingEnabled()) { | ||||
|         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 +708,7 @@ class WebsocketHandler { | ||||
|           freshTxs: fresh, | ||||
|           sigopTxs: sigop, | ||||
|           fullrbfTxs: fullrbf, | ||||
|           acceleratedTxs: accelerated, | ||||
|           matchRate: matchRate, | ||||
|           expectedFees: totalFees, | ||||
|           expectedWeight: totalWeight, | ||||
| @ -770,9 +736,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); | ||||
| @ -784,6 +750,9 @@ class WebsocketHandler { | ||||
|     const fees = feeApi.getRecommendedFee(); | ||||
|     const mempoolInfo = memPool.getMempoolInfo(); | ||||
| 
 | ||||
|     // pre-compute address transactions
 | ||||
|     const addressCache = this.makeAddressCache(transactions); | ||||
| 
 | ||||
|     // update init data
 | ||||
|     this.updateSocketDataFields({ | ||||
|       'mempoolInfo': mempoolInfo, | ||||
| @ -836,51 +805,17 @@ class WebsocketHandler { | ||||
|           if (mempoolTx && mempoolTx.position) { | ||||
|             response['txPosition'] = JSON.stringify({ | ||||
|               txid: trackTxid, | ||||
|               position: mempoolTx.position, | ||||
|               position: { | ||||
|                 ...mempoolTx.position, | ||||
|                 accelerated: mempoolTx.acceleration || undefined, | ||||
|               } | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-address']) { | ||||
|         const foundTransactions: TransactionExtended[] = []; | ||||
| 
 | ||||
|         transactions.forEach((tx) => { | ||||
|           if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) { | ||||
|             foundTransactions.push(tx); | ||||
|             return; | ||||
|           } | ||||
|           if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) { | ||||
|             foundTransactions.push(tx); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         if (foundTransactions.length) { | ||||
|           foundTransactions.forEach((tx) => { | ||||
|             tx.status = { | ||||
|               confirmed: true, | ||||
|               block_height: block.height, | ||||
|               block_hash: block.id, | ||||
|               block_time: block.timestamp, | ||||
|             }; | ||||
|           }); | ||||
| 
 | ||||
|           response['block-transactions'] = JSON.stringify(foundTransactions); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (client['track-scriptpubkey']) { | ||||
|         const foundTransactions: TransactionExtended[] = []; | ||||
| 
 | ||||
|         transactions.forEach((tx) => { | ||||
|           if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) { | ||||
|             foundTransactions.push(tx); | ||||
|             return; | ||||
|           } | ||||
|           if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) { | ||||
|             foundTransactions.push(tx); | ||||
|           } | ||||
|         }); | ||||
|         const foundTransactions: TransactionExtended[] = Array.from(addressCache[client['track-address']]?.values() || []); | ||||
| 
 | ||||
|         if (foundTransactions.length) { | ||||
|           foundTransactions.forEach((tx) => { | ||||
| @ -958,6 +893,52 @@ class WebsocketHandler { | ||||
|         + '}'; | ||||
|   } | ||||
| 
 | ||||
|   private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } { | ||||
|     const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {}; | ||||
|     for (const tx of transactions) { | ||||
|       for (const vin of tx.vin) { | ||||
|         if (vin?.prevout?.scriptpubkey_address) { | ||||
|           if (!addressCache[vin.prevout.scriptpubkey_address]) { | ||||
|             addressCache[vin.prevout.scriptpubkey_address] = new Set(); | ||||
|           } | ||||
|           addressCache[vin.prevout.scriptpubkey_address].add(tx); | ||||
|         } | ||||
|         if (vin?.prevout?.scriptpubkey) { | ||||
|           if (!addressCache[vin.prevout.scriptpubkey]) { | ||||
|             addressCache[vin.prevout.scriptpubkey] = new Set(); | ||||
|           } | ||||
|           addressCache[vin.prevout.scriptpubkey].add(tx); | ||||
|         } | ||||
|       } | ||||
|       for (const vout of tx.vout) { | ||||
|         if (vout?.scriptpubkey_address) { | ||||
|           if (!addressCache[vout?.scriptpubkey_address]) { | ||||
|             addressCache[vout?.scriptpubkey_address] = new Set(); | ||||
|           } | ||||
|           addressCache[vout?.scriptpubkey_address].add(tx); | ||||
|         } | ||||
|         if (vout?.scriptpubkey) { | ||||
|           if (!addressCache[vout.scriptpubkey]) { | ||||
|             addressCache[vout.scriptpubkey] = new Set(); | ||||
|           } | ||||
|           addressCache[vout.scriptpubkey].add(tx); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return addressCache; | ||||
|   } | ||||
| 
 | ||||
|   private async getFullTransactions(transactions: MempoolTransactionExtended[]): Promise<MempoolTransactionExtended[]> { | ||||
|     for (let i = 0; i < transactions.length; i++) { | ||||
|       try { | ||||
|         transactions[i] = await transactionUtils.$getMempoolTransactionExtended(transactions[i].txid, true); | ||||
|       } catch (e) { | ||||
|         logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||
|       } | ||||
|     } | ||||
|     return transactions; | ||||
|   } | ||||
| 
 | ||||
|   private printLogs(): void { | ||||
|     if (this.wss) { | ||||
|       const count = this.wss?.clients?.size || 0; | ||||
|  | ||||
| @ -38,11 +38,13 @@ interface IConfig { | ||||
|     DISK_CACHE_BLOCK_INTERVAL: number; | ||||
|     MAX_PUSH_TX_SIZE_WEIGHT: number; | ||||
|     ALLOW_UNREACHABLE: boolean; | ||||
|     PRICE_UPDATES_PER_HOUR: number; | ||||
|   }; | ||||
|   ESPLORA: { | ||||
|     REST_API_URL: string; | ||||
|     UNIX_SOCKET_PATH: string | void | null; | ||||
|     RETRY_UNIX_SOCKET_AFTER: number; | ||||
|     FALLBACK: string[]; | ||||
|   }; | ||||
|   LIGHTNING: { | ||||
|     ENABLED: boolean; | ||||
| @ -115,10 +117,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 +137,10 @@ interface IConfig { | ||||
|     AUDIT_START_HEIGHT: number; | ||||
|     SERVERS: string[]; | ||||
|   }, | ||||
|   MEMPOOL_SERVICES: { | ||||
|     API: string; | ||||
|     ACCELERATIONS: boolean; | ||||
|   }, | ||||
|   REDIS: { | ||||
|     ENABLED: boolean; | ||||
|     UNIX_SOCKET_PATH: string; | ||||
| @ -181,11 +183,13 @@ const defaults: IConfig = { | ||||
|     'DISK_CACHE_BLOCK_INTERVAL': 6, | ||||
|     'MAX_PUSH_TX_SIZE_WEIGHT': 400000, | ||||
|     'ALLOW_UNREACHABLE': true, | ||||
|     'PRICE_UPDATES_PER_HOUR': 1, | ||||
|   }, | ||||
|   'ESPLORA': { | ||||
|     'REST_API_URL': 'http://127.0.0.1:3000', | ||||
|     'UNIX_SOCKET_PATH': null, | ||||
|     'RETRY_UNIX_SOCKET_AFTER': 30000, | ||||
|     'FALLBACK': [], | ||||
|   }, | ||||
|   'ELECTRUM': { | ||||
|     'HOST': '127.0.0.1', | ||||
| @ -258,10 +262,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 +282,10 @@ const defaults: IConfig = { | ||||
|     'AUDIT_START_HEIGHT': 774000, | ||||
|     'SERVERS': [], | ||||
|   }, | ||||
|   'MEMPOOL_SERVICES': { | ||||
|     'API': '', | ||||
|     'ACCELERATIONS': false, | ||||
|   }, | ||||
|   'REDIS': { | ||||
|     'ENABLED': false, | ||||
|     'UNIX_SOCKET_PATH': '', | ||||
| @ -302,10 +306,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 +327,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'; | ||||
| @ -90,6 +91,10 @@ class Server { | ||||
|   async startServer(worker = false): Promise<void> { | ||||
|     logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); | ||||
| 
 | ||||
|     if (config.MEMPOOL.BACKEND === 'esplora') { | ||||
|       bitcoinApi.startHealthChecks(); | ||||
|     } | ||||
| 
 | ||||
|     if (config.DATABASE.ENABLED) { | ||||
|       await DB.checkDbConnection(); | ||||
|       try { | ||||
| @ -193,6 +198,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 +267,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; | ||||
| } | ||||
| 
 | ||||
| @ -101,6 +104,7 @@ export interface MempoolTransactionExtended extends TransactionExtended { | ||||
|   adjustedFeePerVsize: number; | ||||
|   inputs?: number[]; | ||||
|   lastBoosted?: number; | ||||
|   cpfpDirty?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface AuditTransaction { | ||||
| @ -182,6 +186,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(); | ||||
|  | ||||
							
								
								
									
										179
									
								
								backend/src/utils/p-limit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								backend/src/utils/p-limit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,179 @@ | ||||
| /* | ||||
| MIT License | ||||
| 
 | ||||
| Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
 | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this | ||||
| software and associated documentation files (the "Software"), to deal in the Software | ||||
| without restriction, including without limitation the rights to use, copy, modify, | ||||
| merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||||
| permit persons to whom the Software is furnished to do so, subject to the following | ||||
| conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all copies | ||||
| or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | ||||
| INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A | ||||
| PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | ||||
| HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF | ||||
| CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE | ||||
| OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
| */ | ||||
| 
 | ||||
| /* | ||||
| How it works: | ||||
| `this._head` is an instance of `Node` which keeps track of its current value and nests | ||||
| another instance of `Node` that keeps the value that comes after it. When a value is | ||||
| provided to `.enqueue()`, the code needs to iterate through `this._head`, going deeper | ||||
| and deeper to find the last value. However, iterating through every single item is slow. | ||||
| This problem is solved by saving a reference to the last value as `this._tail` so that | ||||
| it can reference it to add a new value. | ||||
| */ | ||||
| 
 | ||||
| class Node { | ||||
|   value; | ||||
|   next; | ||||
| 
 | ||||
|   constructor(value) { | ||||
|     this.value = value; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class Queue { | ||||
|   private _head; | ||||
|   private _tail; | ||||
|   private _size; | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.clear(); | ||||
|   } | ||||
| 
 | ||||
|   enqueue(value) { | ||||
|     const node = new Node(value); | ||||
| 
 | ||||
|     if (this._head) { | ||||
|       this._tail.next = node; | ||||
|       this._tail = node; | ||||
|     } else { | ||||
|       this._head = node; | ||||
|       this._tail = node; | ||||
|     } | ||||
| 
 | ||||
|     this._size++; | ||||
|   } | ||||
| 
 | ||||
|   dequeue() { | ||||
|     const current = this._head; | ||||
|     if (!current) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this._head = this._head.next; | ||||
|     this._size--; | ||||
|     return current.value; | ||||
|   } | ||||
| 
 | ||||
|   clear() { | ||||
|     this._head = undefined; | ||||
|     this._tail = undefined; | ||||
|     this._size = 0; | ||||
|   } | ||||
| 
 | ||||
|   get size() { | ||||
|     return this._size; | ||||
|   } | ||||
| 
 | ||||
|   *[Symbol.iterator]() { | ||||
|     let current = this._head; | ||||
| 
 | ||||
|     while (current) { | ||||
|       yield current.value; | ||||
|       current = current.next; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| interface LimitFunction { | ||||
|   readonly activeCount: number; | ||||
|   readonly pendingCount: number; | ||||
|   clearQueue: () => void; | ||||
|   <Arguments extends unknown[], ReturnType>( | ||||
|     fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType, | ||||
|     ...args: Arguments | ||||
|   ): Promise<ReturnType>; | ||||
| } | ||||
| 
 | ||||
| export default function pLimit(concurrency: number): LimitFunction { | ||||
|   if ( | ||||
|     !( | ||||
|       (Number.isInteger(concurrency) || | ||||
|         concurrency === Number.POSITIVE_INFINITY) && | ||||
|       concurrency > 0 | ||||
|     ) | ||||
|   ) { | ||||
|     throw new TypeError('Expected `concurrency` to be a number from 1 and up'); | ||||
|   } | ||||
| 
 | ||||
|   const queue = new Queue(); | ||||
|   let activeCount = 0; | ||||
| 
 | ||||
|   const next = () => { | ||||
|     activeCount--; | ||||
| 
 | ||||
|     if (queue.size > 0) { | ||||
|       queue.dequeue()(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const run = async (fn, resolve, args) => { | ||||
|     activeCount++; | ||||
| 
 | ||||
|     const result = (async () => fn(...args))(); | ||||
| 
 | ||||
|     resolve(result); | ||||
| 
 | ||||
|     try { | ||||
|       await result; | ||||
|     } catch {} | ||||
| 
 | ||||
|     next(); | ||||
|   }; | ||||
| 
 | ||||
|   const enqueue = (fn, resolve, args) => { | ||||
|     queue.enqueue(run.bind(undefined, fn, resolve, args)); | ||||
| 
 | ||||
|     (async () => { | ||||
|       // This function needs to wait until the next microtask before comparing
 | ||||
|       // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
 | ||||
|       // when the run function is dequeued and called. The comparison in the if-statement
 | ||||
|       // needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
 | ||||
|       await Promise.resolve(); | ||||
| 
 | ||||
|       if (activeCount < concurrency && queue.size > 0) { | ||||
|         queue.dequeue()(); | ||||
|       } | ||||
|     })(); | ||||
|   }; | ||||
| 
 | ||||
|   const generator = (fn, ...args) => | ||||
|     new Promise((resolve) => { | ||||
|       enqueue(fn, resolve, args); | ||||
|     }); | ||||
| 
 | ||||
|   Object.defineProperties(generator, { | ||||
|     activeCount: { | ||||
|       get: () => activeCount, | ||||
|     }, | ||||
|     pendingCount: { | ||||
|       get: () => queue.size, | ||||
|     }, | ||||
|     clearQueue: { | ||||
|       value: () => { | ||||
|         queue.clear(); | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return generator as any; | ||||
| } | ||||
							
								
								
									
										3
									
								
								contributors/TheBlueMatt.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/TheBlueMatt.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file with sha256 hash c80c5ee4c71c5a76a1f6cd35339bd0c45b25b491933ea7b02a66470e9f43a6fd. | ||||
| 
 | ||||
| Signed: TheBlueMatt | ||||
| @ -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__", | ||||
| @ -50,7 +51,8 @@ | ||||
|   "ESPLORA": { | ||||
|     "REST_API_URL": "__ESPLORA_REST_API_URL__", | ||||
|     "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", | ||||
|     "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__ | ||||
|     "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, | ||||
|     "FALLBACK": __ESPLORA_FALLBACK__ | ||||
|   }, | ||||
|   "SECOND_CORE_RPC": { | ||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", | ||||
| @ -111,10 +113,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 +133,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} | ||||
| @ -53,6 +53,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false} | ||||
| __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} | ||||
| __ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"} | ||||
| __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} | ||||
| __ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]} | ||||
| 
 | ||||
| # SECOND_CORE_RPC | ||||
| __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1} | ||||
| @ -94,10 +95,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 +134,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 +178,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 | ||||
| @ -191,6 +193,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config | ||||
| sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json | ||||
| sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json | ||||
| sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json | ||||
| sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json | ||||
| 
 | ||||
| sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json | ||||
| sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json | ||||
| @ -226,9 +229,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 +267,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 | ||||
|  | ||||
| @ -42,9 +42,6 @@ | ||||
| // -- This will overwrite an existing command --
 | ||||
| // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
 | ||||
| 
 | ||||
| 'use strict' | ||||
| 
 | ||||
| import 'cypress-wait-until'; | ||||
| import { PageIdleDetector } from './PageIdleDetector'; | ||||
| import { mockWebSocket } from './websocket'; | ||||
| 
 | ||||
|  | ||||
| @ -14,6 +14,7 @@ | ||||
| // ***********************************************************
 | ||||
| 
 | ||||
| // When a command from ./commands is ready to use, import with `import './commands'` syntax
 | ||||
| import 'cypress-wait-until'; | ||||
| import './commands'; | ||||
| import failOnConsoleError from 'cypress-fail-on-console-error'; | ||||
| 
 | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   "extends": "../tsconfig.json", | ||||
|   "include": ["**/*.ts"], | ||||
|   "compilerOptions": { | ||||
|     "types": ["cypress"], | ||||
|     "types": ["cypress", "node", "cypress-wait-until"], | ||||
|     "lib": ["es2015", "dom"], | ||||
|     "allowJs": true, | ||||
|     "noEmit": true, | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
							
								
								
									
										3429
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3429
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -61,18 +61,18 @@ | ||||
|     "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@angular-devkit/build-angular": "^16.1.4", | ||||
|     "@angular/animations": "^16.1.5", | ||||
|     "@angular/cli": "^16.1.4", | ||||
|     "@angular/common": "^16.1.5", | ||||
|     "@angular/compiler": "^16.1.5", | ||||
|     "@angular/core": "^16.1.5", | ||||
|     "@angular/forms": "^16.1.5", | ||||
|     "@angular/localize": "^16.1.5", | ||||
|     "@angular/platform-browser": "^16.1.5", | ||||
|     "@angular/platform-browser-dynamic": "^16.1.5", | ||||
|     "@angular/platform-server": "^16.1.5", | ||||
|     "@angular/router": "^16.1.5", | ||||
|     "@angular-devkit/build-angular": "^16.2.0", | ||||
|     "@angular/animations": "^16.2.2", | ||||
|     "@angular/cli": "^16.2.0", | ||||
|     "@angular/common": "^16.2.2", | ||||
|     "@angular/compiler": "^16.2.2", | ||||
|     "@angular/core": "^16.2.2", | ||||
|     "@angular/forms": "^16.2.2", | ||||
|     "@angular/localize": "^16.2.2", | ||||
|     "@angular/platform-browser": "^16.2.2", | ||||
|     "@angular/platform-browser-dynamic": "^16.2.2", | ||||
|     "@angular/platform-server": "^16.2.2", | ||||
|     "@angular/router": "^16.2.2", | ||||
|     "@fortawesome/angular-fontawesome": "~0.13.0", | ||||
|     "@fortawesome/fontawesome-common-types": "~6.4.0", | ||||
|     "@fortawesome/fontawesome-svg-core": "~6.4.0", | ||||
| @ -110,9 +110,10 @@ | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "@cypress/schematic": "^2.5.0", | ||||
|     "cypress": "^12.17.1", | ||||
|     "@types/cypress": "^1.1.3", | ||||
|     "cypress": "^12.17.2", | ||||
|     "cypress-fail-on-console-error": "~4.0.3", | ||||
|     "cypress-wait-until": "^1.7.2", | ||||
|     "cypress-wait-until": "^2.0.0", | ||||
|     "mock-socket": "~9.2.1", | ||||
|     "start-server-and-test": "~2.0.0" | ||||
|   }, | ||||
|  | ||||
| @ -112,6 +112,14 @@ PROXY_CONFIG.push(...[ | ||||
|         "^/testnet": "" | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     context: ['/api/v1/services/**'], | ||||
|     target: `http://localhost:9000`, | ||||
|     secure: false, | ||||
|     ws: true, | ||||
|     changeOrigin: true, | ||||
|     proxyTimeout: 30000, | ||||
|   }, | ||||
|   { | ||||
|     context: ['/api/v1/**'], | ||||
|     target: `http://127.0.0.1:8999`, | ||||
|  | ||||
| @ -112,6 +112,14 @@ PROXY_CONFIG.push(...[ | ||||
|         "^/testnet": "" | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     context: ['/api/v1/services/**'], | ||||
|     target: `http://localhost:9000`, | ||||
|     secure: false, | ||||
|     ws: true, | ||||
|     changeOrigin: true, | ||||
|     proxyTimeout: 30000, | ||||
|   }, | ||||
|   { | ||||
|     context: ['/api/v1/**'], | ||||
|     target: `http://localhost:8999`, | ||||
|  | ||||
| @ -95,6 +95,14 @@ if (configContent && configContent.BASE_MODULE === 'bisq') { | ||||
| } | ||||
| 
 | ||||
| PROXY_CONFIG.push(...[ | ||||
|   { | ||||
|     context: ['/api/v1/services/**'], | ||||
|     target: `http://localhost:9000`, | ||||
|     secure: false, | ||||
|     ws: true, | ||||
|     changeOrigin: true, | ||||
|     proxyTimeout: 30000, | ||||
|   }, | ||||
|   { | ||||
|     context: ['/api/v1/**'], | ||||
|     target: `http://localhost:8999`, | ||||
|  | ||||
| @ -41,12 +41,14 @@ export class BisqAddressComponent implements OnInit, OnDestroy { | ||||
|           document.body.scrollTo(0, 0); | ||||
|           this.addressString = params.get('id') || ''; | ||||
|           this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`); | ||||
| 
 | ||||
|           return this.bisqApiService.getAddress$(this.addressString) | ||||
|             .pipe( | ||||
|               catchError((err) => { | ||||
|                 this.isLoadingAddress = false; | ||||
|                 this.error = err; | ||||
|                 this.seoService.logSoft404(); | ||||
|                 console.log(err); | ||||
|                 return of(null); | ||||
|               }) | ||||
| @ -62,6 +64,7 @@ export class BisqAddressComponent implements OnInit, OnDestroy { | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
|         this.error = error; | ||||
|         this.seoService.logSoft404(); | ||||
|         this.isLoadingAddress = false; | ||||
|       }); | ||||
|   } | ||||
|  | ||||
| @ -82,11 +82,13 @@ export class BisqBlockComponent implements OnInit, OnDestroy { | ||||
|       ) | ||||
|       .subscribe((block: BisqBlock) => { | ||||
|         if (!block) { | ||||
|           this.seoService.logSoft404(); | ||||
|           return; | ||||
|         } | ||||
|         this.isLoading = false; | ||||
|         this.blockHeight = block.height; | ||||
|         this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`); | ||||
|         this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`); | ||||
|         this.block = block; | ||||
|       }); | ||||
|   } | ||||
| @ -97,6 +99,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   caughtHttpError(err: HttpErrorResponse){ | ||||
|     this.error = err; | ||||
|     this.seoService.logSoft404(); | ||||
|     return of(null); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -36,6 +36,7 @@ export class BisqBlocksComponent implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|     this.websocketService.want(['blocks']); | ||||
|     this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`); | ||||
|     this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); | ||||
|     this.loadingItems = Array(this.itemsPerPage); | ||||
|     if (document.body.clientWidth < 670) { | ||||
|  | ||||
| @ -29,7 +29,8 @@ export class BisqDashboardComponent implements OnInit { | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle(`Markets`); | ||||
|     this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`); | ||||
|     this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|     this.volumes$ = this.bisqApiService.getAllVolumesDay$() | ||||
|  | ||||
| @ -34,6 +34,7 @@ export class BisqMainDashboardComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.resetTitle(); | ||||
|     this.seoService.resetDescription(); | ||||
|     this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|     this.usdPrice$ = this.stateService.conversions$.asObservable().pipe( | ||||
|  | ||||
| @ -48,7 +48,8 @@ export class BisqMarketComponent implements OnInit, OnDestroy { | ||||
|         map(([markets, routeParams]) => { | ||||
|           const pair = routeParams.get('pair'); | ||||
|           const pairUpperCase = pair.replace('_', '/').toUpperCase(); | ||||
|           this.seoService.setTitle(`Bisq market: ${pairUpperCase}`); | ||||
|           this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`); | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`); | ||||
| 
 | ||||
|           return { | ||||
|             pair: pairUpperCase, | ||||
|  | ||||
| @ -26,6 +26,7 @@ export class BisqStatsComponent implements OnInit { | ||||
|     this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|     this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`); | ||||
|     this.stateService.bsqPrice$ | ||||
|       .subscribe((bsqPrice) => { | ||||
|         this.price = bsqPrice; | ||||
|  | ||||
| @ -48,6 +48,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy { | ||||
|         document.body.scrollTo(0, 0); | ||||
|         this.txId = params.get('id') || ''; | ||||
|         this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`); | ||||
|         this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`); | ||||
|         if (history.state.data) { | ||||
|           return of(history.state.data); | ||||
|         } | ||||
| @ -70,11 +71,13 @@ export class BisqTransactionComponent implements OnInit, OnDestroy { | ||||
|                     catchError((txError: HttpErrorResponse) => { | ||||
|                       console.log(txError); | ||||
|                       this.error = txError; | ||||
|                       this.seoService.logSoft404(); | ||||
|                       return of(null); | ||||
|                     }) | ||||
|                   ); | ||||
|               } | ||||
|               this.error = bisqTxError; | ||||
|               this.seoService.logSoft404(); | ||||
|               return of(null); | ||||
|             }) | ||||
|           ); | ||||
| @ -103,6 +106,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy { | ||||
|       this.isLoadingTx = false; | ||||
| 
 | ||||
|       if (!tx) { | ||||
|         this.seoService.logSoft404(); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -79,6 +79,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy { | ||||
|   ngOnInit(): void { | ||||
|     this.websocketService.want(['blocks']); | ||||
|     this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`); | ||||
| 
 | ||||
|     this.radioGroupForm = this.formBuilder.group({ | ||||
|       txTypes: [this.txTypesDefaultChecked], | ||||
|  | ||||
| @ -4,7 +4,8 @@ | ||||
|     <span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">®</span> | ||||
|     <img class="logo" src="/resources/mempool-logo-bigger.png" /> | ||||
|     <div class="version"> | ||||
|       v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>] | ||||
|       <span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span> | ||||
|       <span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
| @ -31,6 +32,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"> | ||||
| @ -173,34 +182,44 @@ | ||||
|         </svg> | ||||
|         <span>Exodus</span> | ||||
|       </a> | ||||
|       <a href="https://www.luminex.io" target="_blank" title="Luminex"> | ||||
|         <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;"> | ||||
|           <defs> | ||||
|             <style> | ||||
|               .lum-cls-1 { | ||||
|                 fill: #f2ea25; | ||||
|               } | ||||
|             </style> | ||||
|           </defs> | ||||
|           <path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/> | ||||
|           <path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/> | ||||
|           <rect class="lum-cls-1" width="60.69" height="372.67"/> | ||||
|         </svg> | ||||
|         <span>Luminex</span> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <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> | ||||
|             <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="sponsors$ | async as sponsors; else loadingSponsors"> | ||||
|         <ng-template ngFor let-sponsor [ngForOf]="sponsors"> | ||||
|           <a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle"> | ||||
|             <img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" /> | ||||
|           </a> | ||||
|         </ng-template> | ||||
|       <ng-container *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> | ||||
| @ -224,7 +243,7 @@ | ||||
|         <img class="image" src="/resources/profile/ronindojo.png" /> | ||||
|         <span>RoninDojo</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/runcitadel/core" target="_blank" title="Citadel"> | ||||
|       <a href="https://github.com/runcitadel" target="_blank" title="Citadel"> | ||||
|         <img class="image" src="/resources/profile/runcitadel.svg" /> | ||||
|         <span>Citadel</span> | ||||
|       </a> | ||||
| @ -340,7 +359,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 +373,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 +385,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; | ||||
| @ -19,6 +22,7 @@ | ||||
| 
 | ||||
|   .intro { | ||||
|     margin: 25px auto 30px; | ||||
|     margin-top: 25px; | ||||
|     width: 250px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|  | ||||
| @ -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, | ||||
| @ -41,12 +43,16 @@ export class AboutComponent implements OnInit { | ||||
|   ngOnInit() { | ||||
|     this.backendInfo$ = this.stateService.backendInfo$; | ||||
|     this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project™\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`); | ||||
|     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 +65,9 @@ export class AboutComponent implements OnInit { | ||||
|         }), | ||||
|         tap(() => this.goToAnchor()) | ||||
|       ); | ||||
| 
 | ||||
|     this.ogs$ = this.apiService.getOgs$(); | ||||
| 
 | ||||
|     this.allContributors$ = this.apiService.getContributor$().pipe( | ||||
|       map((contributors) => { | ||||
|         return { | ||||
|  | ||||
| @ -0,0 +1,21 @@ | ||||
| <div class="fee-graph" *ngIf="tx && estimate"> | ||||
|   <div class="column"> | ||||
|     <ng-container *ngFor="let bar of bars"> | ||||
|       <div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);"> | ||||
|         <div class="fill"></div> | ||||
|         <div class="line"> | ||||
|           <p class="fee-rate"> | ||||
|             <span class="label">{{ bar.label }}</span> | ||||
|             <span class="rate"> | ||||
|               <app-fee-rate [fee]="bar.rate"></app-fee-rate> | ||||
|             </span> | ||||
|           </p> | ||||
|         </div> | ||||
|         <div class="spacer"></div> | ||||
|         <span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span> | ||||
|         <div class="spacer"></div> | ||||
|         <div class="spacer"></div> | ||||
|       </div> | ||||
|     </ng-container> | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,157 @@ | ||||
| .fee-graph { | ||||
|   height: 100%; | ||||
|   min-width: 120px; | ||||
|   width: 120px; | ||||
|   max-height: 90vh; | ||||
|   margin-left: 4em; | ||||
|   margin-right: 1.5em; | ||||
|   padding-bottom: 63px; | ||||
| 
 | ||||
|   .column { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     position: relative; | ||||
|     background: #181b2d; | ||||
| 
 | ||||
|     .bar { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|       right: 0; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
| 
 | ||||
|       .fill { | ||||
|         position: absolute; | ||||
|         left: 0; | ||||
|         right: 0; | ||||
|         top: 0; | ||||
|         bottom: 0; | ||||
|         opacity: 0.75; | ||||
|         pointer-events: none; | ||||
|       } | ||||
| 
 | ||||
|       .fee { | ||||
|         font-size: 0.9em; | ||||
|         opacity: 0; | ||||
|         pointer-events: none; | ||||
|       } | ||||
| 
 | ||||
|       .spacer { | ||||
|         width: 100%; | ||||
|         height: 1px; | ||||
|         flex-grow: 1; | ||||
|         pointer-events: none; | ||||
|       } | ||||
| 
 | ||||
|       .line { | ||||
|         position: absolute; | ||||
|         right: 0; | ||||
|         top: 0; | ||||
|         left: -4.5em; | ||||
|         border-top: dashed white 1.5px; | ||||
| 
 | ||||
|         .fee-rate { | ||||
|           width: 100%; | ||||
|           position: absolute; | ||||
|           left: 0; | ||||
|           right: 0.2em; | ||||
|           font-size: 0.8em; | ||||
|           display: flex; | ||||
|           flex-direction: row-reverse; | ||||
|           justify-content: space-between; | ||||
|           margin: 0; | ||||
| 
 | ||||
|           .label { | ||||
|             margin-right: .2em; | ||||
|           } | ||||
| 
 | ||||
|           .rate .symbol { | ||||
|             color: white; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       &.tx { | ||||
|         .fill { | ||||
|           background: #3bcc49; | ||||
|         } | ||||
|         .line { | ||||
|           .fee-rate { | ||||
|             top: 0; | ||||
|           } | ||||
|         } | ||||
|         .fee { | ||||
|           position: absolute; | ||||
|           opacity: 1; | ||||
|           z-index: 11; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       &.target { | ||||
|         .fill { | ||||
|           background: #653b9c; | ||||
|         } | ||||
|         .fee { | ||||
|           position: absolute; | ||||
|           opacity: 1; | ||||
|           z-index: 11; | ||||
|         } | ||||
|         .line .fee-rate { | ||||
|           bottom: 2px; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       &.max { | ||||
|         cursor: pointer; | ||||
|         .line .fee-rate { | ||||
|           .label { | ||||
|             opacity: 0; | ||||
|           } | ||||
|           bottom: 2px; | ||||
|         } | ||||
|         &.active, &:hover { | ||||
|           .fill { | ||||
|             background: #105fb0; | ||||
|           } | ||||
|           .line { | ||||
|             .fee-rate .label { | ||||
|               opacity: 1; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       &:hover { | ||||
|         .fill { | ||||
|           z-index: 10; | ||||
|         } | ||||
|         .line { | ||||
|           z-index: 11; | ||||
|         } | ||||
|         .fee { | ||||
|           opacity: 1; | ||||
|           z-index: 12; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &:hover > .bar:not(:hover) { | ||||
|       &.target, &.max { | ||||
|         .fee { | ||||
|           opacity: 0; | ||||
|         } | ||||
|         .line .fee-rate .label { | ||||
|           opacity: 0; | ||||
|         } | ||||
|       } | ||||
|       &.max { | ||||
|         .fill { | ||||
|           background: none; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,96 @@ | ||||
| import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { ReplaySubject, merge, Subscription, of } from 'rxjs'; | ||||
| import { tap, switchMap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { AccelerationEstimate, RateOption } from './accelerate-preview.component'; | ||||
| 
 | ||||
| interface GraphBar { | ||||
|   rate: number; | ||||
|   style: any; | ||||
|   class: 'tx' | 'target' | 'max'; | ||||
|   label: string; | ||||
|   active?: boolean; | ||||
|   rateIndex?: number; | ||||
|   fee?: number; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerate-fee-graph', | ||||
|   templateUrl: './accelerate-fee-graph.component.html', | ||||
|   styleUrls: ['./accelerate-fee-graph.component.scss'], | ||||
| }) | ||||
| export class AccelerateFeeGraphComponent implements OnInit, OnChanges { | ||||
|   @Input() tx: Transaction; | ||||
|   @Input() estimate: AccelerationEstimate; | ||||
|   @Input() maxRateOptions: RateOption[] = []; | ||||
|   @Input() maxRateIndex: number = 0; | ||||
|   @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); | ||||
| 
 | ||||
|   bars: GraphBar[] = []; | ||||
|   tooltipPosition = { x: 0, y: 0 }; | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.initGraph(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|     this.initGraph(); | ||||
|   } | ||||
| 
 | ||||
|   initGraph(): void { | ||||
|     if (!this.tx || !this.estimate) { | ||||
|       return; | ||||
|     } | ||||
|     const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate)); | ||||
|     const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize; | ||||
|     const baseHeight = baseRate / maxRate; | ||||
|     const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { | ||||
|       return { | ||||
|         rate: option.rate, | ||||
|         style: this.getStyle(option.rate, maxRate, baseHeight), | ||||
|         class: 'max', | ||||
|         label: 'maximum', | ||||
|         active: option.index === this.maxRateIndex, | ||||
|         rateIndex: option.index, | ||||
|         fee: option.fee, | ||||
|       } | ||||
|     }); | ||||
|     bars.push({ | ||||
|       rate: this.estimate.targetFeeRate, | ||||
|       style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), | ||||
|       class: 'target', | ||||
|       label: 'next block', | ||||
|       fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee | ||||
|     }); | ||||
|     bars.push({ | ||||
|       rate: baseRate, | ||||
|       style: this.getStyle(baseRate, maxRate, 0), | ||||
|       class: 'tx', | ||||
|       label: '', | ||||
|       fee: this.estimate.txSummary.effectiveFee, | ||||
|     }); | ||||
|     this.bars = bars; | ||||
|   } | ||||
| 
 | ||||
|   getStyle(rate, maxRate, base) { | ||||
|     const top = (rate / maxRate); | ||||
|     return { | ||||
|       height: `${(top - base) * 100}%`, | ||||
|       bottom: base ? `${base * 100}%` : '0', | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onClick(event, bar): void { | ||||
|     if (bar.rateIndex != null) { | ||||
|       this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('pointermove', ['$event']) | ||||
|   onPointerMove(event) { | ||||
|     this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,262 @@ | ||||
| <div class="row" *ngIf="showSuccess"> | ||||
|   <div class="col" id="successAlert"> | ||||
|     <div class="alert alert-success"> | ||||
|       Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>. | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <div class="row" *ngIf="error"> | ||||
|   <div class="col" id="mempoolError"> | ||||
|     <app-mempool-error [error]="error"></app-mempool-error> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <div class="accelerate-cols"> | ||||
|   <ng-container *ngIf="!isMobile"> | ||||
|     <app-accelerate-fee-graph | ||||
|       [tx]="tx" | ||||
|       [estimate]="estimate" | ||||
|       [maxRateOptions]="maxRateOptions" | ||||
|       [maxRateIndex]="selectFeeRateIndex" | ||||
|       (setUserBid)="setUserBid($event)" | ||||
|     ></app-accelerate-fee-graph> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <ng-container *ngIf="estimate"> | ||||
|     <div [class]="{estimateDisabled: error}"> | ||||
|       <h5>Your transaction</h5> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> | ||||
|             Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}. | ||||
|           </small> | ||||
|           <table class="table table-borderless table-border table-dark table-accelerator"> | ||||
|             <tbody> | ||||
|               <tr class="group-first"> | ||||
|                 <td class="item"> | ||||
|                   Virtual size | ||||
|                 </td> | ||||
|                 <td class="units" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> | ||||
|               </tr> | ||||
|               <tr class="info"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="item"> | ||||
|                   In-band fees | ||||
|                 </td> | ||||
|                 <td class="units"> | ||||
|                   {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr class="info group-last"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <h5>How much more are you willing to pay?</h5> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <small class="form-text text-muted mb-2"> | ||||
|             Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br> | ||||
|             If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request. | ||||
|           </small> | ||||
|           <div class="form-group"> | ||||
|             <div class="fee-card"> | ||||
|               <div class="d-flex mb-0"> | ||||
|                 <ng-container *ngFor="let option of maxRateOptions"> | ||||
|                   <button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)"> | ||||
|                     <span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span> | ||||
|                     <span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> | ||||
|                   </button> | ||||
|                 </ng-container> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <h5>Acceleration summary</h5> | ||||
|       <div class="row mb-3"> | ||||
|         <div class="col"> | ||||
|           <div class="table-toggle btn-group btn-group-toggle"> | ||||
|             <div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'"> | ||||
|               <span>Estimated cost</span> | ||||
|             </div> | ||||
|             <div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'"> | ||||
|               <span>Maximum cost</span> | ||||
|             </div> | ||||
|           </div> | ||||
|           <table class="table table-borderless table-border table-dark table-accelerator"> | ||||
|             <tbody> | ||||
|               <!-- ESTIMATED FEE --> | ||||
|               <ng-container *ngIf="showTable === 'estimated'"> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item"> | ||||
|                     Next block market rate | ||||
|                   </td> | ||||
|                   <td class="amt" style="font-size: 20px"> | ||||
|                     {{ estimate.targetFeeRate | number : '1.0-0' }} | ||||
|                   </td> | ||||
|                   <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> | ||||
|                 </tr> | ||||
|                 <tr class="info"> | ||||
|                   <td class="info"> | ||||
|                     <i><small>Estimated extra fee required</small></i> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                     <span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|               <!-- USER MAX BID --> | ||||
|               <ng-container *ngIf="showTable === 'maximum'"> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item"> | ||||
|                     Your maximum | ||||
|                   </td> | ||||
|                   <td class="amt" style="width: 45%; font-size: 20px"> | ||||
|                     ~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} | ||||
|                   </td> | ||||
|                   <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> | ||||
|                 </tr> | ||||
|                 <tr class="info"> | ||||
|                   <td class="info"> | ||||
|                     <i><small>The maximum extra transaction fee you could pay</small></i> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     <span> | ||||
|                       {{ userBid | number }} | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                     <span class="fiat"><app-fiat [value]="userBid"></app-fiat></span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|    | ||||
|               <!-- MEMPOOL BASE FEE --> | ||||
|               <tr> | ||||
|                 <td class="item"> | ||||
|                   Mempool Accelerator™ fees | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr class="info"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>mempool.space fee</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.mempoolBaseFee | number }} | ||||
|                 </td> | ||||
|                 <td class="units"> | ||||
|                   <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                   <span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr class="info group-last" style="border-bottom: 1px solid lightgrey"> | ||||
|                 <td class="info"> | ||||
|                   <i><small>Transaction vsize fee</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.vsizeFee | number }} | ||||
|                 </td> | ||||
|                 <td class="units"> | ||||
|                   <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                   <span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> | ||||
|                 </td> | ||||
|               </tr> | ||||
| 
 | ||||
|               <!-- NEXT BLOCK ESTIMATE --> | ||||
|               <ng-container *ngIf="showTable === 'estimated'"> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item"> | ||||
|                     <b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     <span style="background-color: #5E35B1" class="p-1 pl-0"> | ||||
|                       {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                     <span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|                 <tr class="info group-last"> | ||||
|                   <td class="info"> | ||||
|                     <i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|    | ||||
|               <!-- MAX COST --> | ||||
|               <ng-container *ngIf="showTable === 'maximum'"> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item"> | ||||
|                     <b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     <span style="background-color: #105fb0" class="p-1 pl-0"> | ||||
|                       {{ maxCost | number }} | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                     <span class="fiat"> | ||||
|                       <app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|                 <tr class="info group-last"> | ||||
|                   <td class="info"> | ||||
|                     <i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}  <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|    | ||||
|               <!-- USER BALANCE --> | ||||
|               <ng-container *ngIf="estimate.userBalance < maxCost"> | ||||
|                 <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||
|                   <td class="item"> | ||||
|                     Available balance | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     {{ estimate.userBalance | number }} | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats|sats">sats</span> | ||||
|                     <span class="fiat"> | ||||
|                       <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="row mb-3" *ngIf="isLoggedIn()"> | ||||
|         <div class="col"> | ||||
|           <div class="d-flex justify-content-end"> | ||||
|             <button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|     </div> | ||||
|   </ng-container> | ||||
| </div> | ||||
| @ -0,0 +1,88 @@ | ||||
| .fee-card { | ||||
|   padding: 15px; | ||||
|   background-color: #1d1f31; | ||||
| 
 | ||||
|   .feerate { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     .fee { | ||||
|       font-size: 1.2em; | ||||
|     } | ||||
|     .rate { | ||||
|       font-size: 0.9em; | ||||
|       .symbol { | ||||
|         color: white; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .btn-border { | ||||
|   border: solid 1px black; | ||||
|   background-color: #0c4a87; | ||||
| } | ||||
| 
 | ||||
| .feerate.active { | ||||
|   background-color: #105fb0 !important; | ||||
|   opacity: 1; | ||||
|   border: 1px solid white !important; | ||||
| } | ||||
| 
 | ||||
| .estimateDisabled { | ||||
|   opacity: 0.5; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .table-toggle { | ||||
|   width: 100%; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .table-accelerator { | ||||
|   tr { | ||||
|     text-wrap: wrap; | ||||
| 
 | ||||
|     td { | ||||
|       padding-top: 0; | ||||
|       padding-bottom: 0; | ||||
|       vertical-align: baseline; | ||||
|     } | ||||
| 
 | ||||
|     &.group-first { | ||||
|       td { | ||||
|         padding-top: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|     &.group-last { | ||||
|       td { | ||||
|         padding-bottom: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   td { | ||||
|     &:first-child { | ||||
|       width: 100vw; | ||||
|     } | ||||
|     &.info { | ||||
|       color: #6c757d; | ||||
|     } | ||||
|     &.amt { | ||||
|       text-align: right; | ||||
|       padding-right: 0.2em; | ||||
|     } | ||||
|     &.units { | ||||
|       padding-left: 0.2em; | ||||
|       white-space: nowrap; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .accelerate-cols { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: stretch; | ||||
|   margin-top: 1em; | ||||
| } | ||||
| @ -0,0 +1,205 @@ | ||||
| import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { Subscription, catchError, of, tap } from 'rxjs'; | ||||
| import { StorageService } from '../../services/storage.service'; | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| import { nextRoundNumber } from '../../shared/common.utils'; | ||||
| 
 | ||||
| export type AccelerationEstimate = { | ||||
|   txSummary: TxSummary; | ||||
|   nextBlockFee: number; | ||||
|   targetFeeRate: number; | ||||
|   userBalance: number; | ||||
|   enoughBalance: boolean; | ||||
|   cost: number; | ||||
|   mempoolBaseFee: number; | ||||
|   vsizeFee: number; | ||||
| } | ||||
| export type TxSummary = { | ||||
|   txid: string; // txid of the current transaction
 | ||||
|   effectiveVsize: number; // Total vsize of the dependency tree
 | ||||
|   effectiveFee: number;  // Total fee of the dependency tree in sats
 | ||||
|   ancestorCount: number; // Number of ancestors
 | ||||
| } | ||||
| 
 | ||||
| export interface RateOption { | ||||
|   fee: number; | ||||
|   rate: number; | ||||
|   index: number; | ||||
| } | ||||
| 
 | ||||
| export const MIN_BID_RATIO = 1; | ||||
| export const DEFAULT_BID_RATIO = 2; | ||||
| export const MAX_BID_RATIO = 4; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerate-preview', | ||||
|   templateUrl: 'accelerate-preview.component.html', | ||||
|   styleUrls: ['accelerate-preview.component.scss'] | ||||
| }) | ||||
| export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { | ||||
|   @Input() tx: Transaction | undefined; | ||||
|   @Input() scrollEvent: boolean; | ||||
| 
 | ||||
|   math = Math; | ||||
|   error = ''; | ||||
|   showSuccess = false; | ||||
|   estimateSubscription: Subscription; | ||||
|   accelerationSubscription: Subscription; | ||||
|   estimate: any; | ||||
|   hasAncestors: boolean = false; | ||||
|   minExtraCost = 0; | ||||
|   minBidAllowed = 0; | ||||
|   maxBidAllowed = 0; | ||||
|   defaultBid = 0; | ||||
|   maxCost = 0; | ||||
|   userBid = 0; | ||||
|   selectFeeRateIndex = 1; | ||||
|   showTable: 'estimated' | 'maximum' = 'maximum'; | ||||
|   isMobile: boolean = window.innerWidth <= 767.98; | ||||
| 
 | ||||
|   maxRateOptions: RateOption[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     private apiService: ApiService, | ||||
|     private storageService: StorageService | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.estimateSubscription) { | ||||
|       this.estimateSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.scrollEvent) { | ||||
|       this.scrollToPreview('acceleratePreviewAnchor', 'center'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe( | ||||
|       tap((response) => { | ||||
|         if (response.status === 204) { | ||||
|           this.estimate = undefined; | ||||
|           this.error = `cannot_accelerate_tx`; | ||||
|           this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|           this.estimateSubscription.unsubscribe(); | ||||
|         } else { | ||||
|           this.estimate = response.body; | ||||
|           if (!this.estimate) { | ||||
|             this.error = `cannot_accelerate_tx`; | ||||
|             this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|             this.estimateSubscription.unsubscribe(); | ||||
|           } | ||||
| 
 | ||||
|           if (this.estimate.userBalance <= 0) { | ||||
|             if (this.isLoggedIn()) { | ||||
|               this.error = `not_enough_balance`; | ||||
|               this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; | ||||
|            | ||||
|           // Make min extra fee at least 50% of the current tx fee
 | ||||
|           this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); | ||||
| 
 | ||||
|           this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { | ||||
|             return { | ||||
|               fee: this.minExtraCost * multiplier, | ||||
|               rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize, | ||||
|               index, | ||||
|             }; | ||||
|           }); | ||||
| 
 | ||||
|           this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO; | ||||
|           this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO; | ||||
|           this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO; | ||||
| 
 | ||||
|           this.userBid = this.defaultBid; | ||||
|           if (this.userBid < this.minBidAllowed) { | ||||
|             this.userBid = this.minBidAllowed; | ||||
|           } else if (this.userBid > this.maxBidAllowed) { | ||||
|             this.userBid = this.maxBidAllowed; | ||||
|           }             | ||||
|           this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
| 
 | ||||
|           if (!this.error) { | ||||
|             this.scrollToPreview('acceleratePreviewAnchor', 'center'); | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       catchError((response) => { | ||||
|         this.estimate = undefined; | ||||
|         this.error = response.error; | ||||
|         this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|         return of(null); | ||||
|       }) | ||||
|     ).subscribe(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * User changed his bid | ||||
|    */ | ||||
|   setUserBid({ fee, index }: { fee: number, index: number}) { | ||||
|     if (this.estimate) { | ||||
|       this.selectFeeRateIndex = index; | ||||
|       this.userBid = Math.max(0, fee); | ||||
|       this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Scroll to element id with or without setTimeout | ||||
|    */ | ||||
|   scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) { | ||||
|     setTimeout(() => { | ||||
|       this.scrollToPreview(id, position); | ||||
|     }, 100); | ||||
|   } | ||||
|   scrollToPreview(id: string, position: ScrollLogicalPosition) { | ||||
|     const acceleratePreviewAnchor = document.getElementById(id); | ||||
|     if (acceleratePreviewAnchor) { | ||||
|       acceleratePreviewAnchor.scrollIntoView({ | ||||
|         behavior: 'smooth', | ||||
|         inline: position, | ||||
|         block: position, | ||||
|       }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|   /** | ||||
|    * Send acceleration request | ||||
|    */ | ||||
|   accelerate() { | ||||
|     if (this.accelerationSubscription) { | ||||
|       this.accelerationSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.accelerationSubscription = this.apiService.accelerate$( | ||||
|       this.tx.txid, | ||||
|       this.userBid | ||||
|     ).subscribe({ | ||||
|       next: () => { | ||||
|         this.showSuccess = true; | ||||
|         this.scrollToPreviewWithTimeout('successAlert', 'center'); | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|       }, | ||||
|       error: (response) => { | ||||
|         this.error = response.error; | ||||
|         this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   isLoggedIn() { | ||||
|     const auth = this.storageService.getAuth(); | ||||
|     return auth !== null; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|   } | ||||
| } | ||||
| @ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { of, merge, Subscription, Observable } from 'rxjs'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { AddressInformation } from '../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -68,6 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { | ||||
|             this.addressString = this.addressString.toLowerCase(); | ||||
|           } | ||||
|           this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); | ||||
| 
 | ||||
|           return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) | ||||
|               ? this.electrsApiService.getPubKeyAddress$(this.addressString) | ||||
|  | ||||
| @ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { of, merge, Subscription, Observable } from 'rxjs'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { AddressInformation } from '../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -76,6 +77,7 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|             this.addressString = this.addressString.toLowerCase(); | ||||
|           } | ||||
|           this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); | ||||
| 
 | ||||
|           return merge( | ||||
|             of(true), | ||||
| @ -91,6 +93,7 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|                 catchError((err) => { | ||||
|                   this.isLoadingAddress = false; | ||||
|                   this.error = err; | ||||
|                   this.seoService.logSoft404(); | ||||
|                   console.log(err); | ||||
|                   return of(null); | ||||
|                 }) | ||||
| @ -162,6 +165,7 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
|         this.error = error; | ||||
|         this.seoService.logSoft404(); | ||||
|         this.isLoadingAddress = false; | ||||
|       }); | ||||
| 
 | ||||
|  | ||||
| @ -86,6 +86,7 @@ export class AssetComponent implements OnInit, OnDestroy { | ||||
|                   catchError((err) => { | ||||
|                     this.isLoadingAsset = false; | ||||
|                     this.error = err; | ||||
|                     this.seoService.logSoft404(); | ||||
|                     console.log(err); | ||||
|                     return of(null); | ||||
|                   }) | ||||
| @ -153,6 +154,7 @@ export class AssetComponent implements OnInit, OnDestroy { | ||||
|       (error) => { | ||||
|         console.log(error); | ||||
|         this.error = error; | ||||
|         this.seoService.logSoft404(); | ||||
|         this.isLoadingAsset = false; | ||||
|       }); | ||||
| 
 | ||||
|  | ||||
| @ -40,6 +40,7 @@ export class AssetsNavComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.liquid.assets:Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more.`); | ||||
|     this.typeaheadSearchFn = this.typeaheadSearch; | ||||
| 
 | ||||
|     this.searchForm = this.formBuilder.group({ | ||||
|  | ||||
| @ -64,6 +64,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`); | ||||
|     this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
|  | ||||
| @ -65,6 +65,7 @@ export class BlockFeesGraphComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees:See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.`); | ||||
|     this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
| @ -192,7 +193,7 @@ export class BlockFeesGraphComponent implements OnInit { | ||||
|           { | ||||
|             name: 'Fees ' + this.currency, | ||||
|             inactiveColor: 'rgb(110, 112, 121)', | ||||
|             textStyle: {   | ||||
|             textStyle: { | ||||
|               color: 'white', | ||||
|             }, | ||||
|             icon: 'roundRect', | ||||
|  | ||||
| @ -61,6 +61,7 @@ export class BlockHealthGraphComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.`); | ||||
|     this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
 | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
|  | ||||
| @ -70,9 +70,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false); | ||||
|     this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false); | ||||
|     this.gl = this.canvas.nativeElement.getContext('webgl'); | ||||
|     this.initCanvas(); | ||||
| 
 | ||||
|     this.resizeCanvas(); | ||||
|     if (this.gl) { | ||||
|       this.initCanvas(); | ||||
|       this.resizeCanvas(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes): void { | ||||
| @ -147,7 +149,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(); | ||||
| @ -195,10 +197,16 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|     cancelAnimationFrame(this.animationFrameRequest); | ||||
|     this.animationFrameRequest = null; | ||||
|     this.running = false; | ||||
|     this.gl = null; | ||||
|   } | ||||
| 
 | ||||
|   handleContextRestored(event): void { | ||||
|     this.initCanvas(); | ||||
|     if (this.canvas?.nativeElement) { | ||||
|       this.gl = this.canvas.nativeElement.getContext('webgl'); | ||||
|       if (this.gl) { | ||||
|         this.initCanvas(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
| @ -224,6 +232,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   } | ||||
| 
 | ||||
|   compileShader(src, type): WebGLShader { | ||||
|     if (!this.gl) { | ||||
|       return; | ||||
|     } | ||||
|     const shader = this.gl.createShader(type); | ||||
| 
 | ||||
|     this.gl.shaderSource(shader, src); | ||||
| @ -237,6 +248,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|   } | ||||
| 
 | ||||
|   buildShaderProgram(shaderInfo): WebGLProgram { | ||||
|     if (!this.gl) { | ||||
|       return; | ||||
|     } | ||||
|     const program = this.gl.createProgram(); | ||||
| 
 | ||||
|     shaderInfo.forEach((desc) => { | ||||
| @ -273,7 +287,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | ||||
|       now = performance.now(); | ||||
|     } | ||||
|     // skip re-render if there's no change to the scene
 | ||||
|     if (this.scene) { | ||||
|     if (this.scene && this.gl) { | ||||
|       /* SET UP SHADER UNIFORMS */ | ||||
|       // screen dimensions
 | ||||
|       this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight); | ||||
|  | ||||
| @ -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,7 +233,11 @@ export default class TxView implements TransactionStripped { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|       default: | ||||
|         return feeLevelColor; | ||||
|         if (this.acc) { | ||||
|           return auditColors.accelerated; | ||||
|         } else { | ||||
|           return feeLevelColor; | ||||
|         } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -63,6 +63,7 @@ export class BlockRewardsGraphComponent implements OnInit { | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-rewards:See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.`); | ||||
|     this.miningWindowPreference = this.miningService.getDefaultTimespan('3m'); | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
| @ -191,7 +192,7 @@ export class BlockRewardsGraphComponent implements OnInit { | ||||
|           { | ||||
|             name: 'Rewards ' + this.currency, | ||||
|             inactiveColor: 'rgb(110, 112, 121)', | ||||
|             textStyle: {   | ||||
|             textStyle: { | ||||
|               color: 'white', | ||||
|             }, | ||||
|             icon: 'roundRect', | ||||
|  | ||||
| @ -60,6 +60,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit { | ||||
|     let firstRun = true; | ||||
| 
 | ||||
|     this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`); | ||||
|     this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-sizes:See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.`); | ||||
|     this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); | ||||
|     this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); | ||||
|     this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); | ||||
|  | ||||
| @ -8,6 +8,7 @@ import { SeoService } from '../../services/seo.service'; | ||||
| import { OpenGraphService } from '../../services/opengraph.service'; | ||||
| import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -82,6 +83,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { | ||||
|               }), | ||||
|               catchError((err) => { | ||||
|                 this.error = err; | ||||
|                 this.seoService.logSoft404(); | ||||
|                 this.openGraphService.fail('block-data-' + this.rawId); | ||||
|                 this.openGraphService.fail('block-viz-' + this.rawId); | ||||
|                 return of(null); | ||||
| @ -96,6 +98,11 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { | ||||
|         this.blockHeight = block.height; | ||||
| 
 | ||||
|         this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`); | ||||
|         if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) { | ||||
|           this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||
|         } else { | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||
|         } | ||||
|         this.isLoadingBlock = false; | ||||
|         this.setBlockSubsidy(); | ||||
|         if (block?.extras?.reward !== undefined) { | ||||
| @ -138,6 +145,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { | ||||
|     (error) => { | ||||
|       this.error = error; | ||||
|       this.isLoadingOverview = false; | ||||
|       this.seoService.logSoft404(); | ||||
|       this.openGraphService.fail('block-viz-' + this.rawId); | ||||
|       this.openGraphService.fail('block-data-' + this.rawId); | ||||
|       if (this.blockGraph) { | ||||
|  | ||||
| @ -13,6 +13,7 @@ import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; | ||||
| import { detectWebGL } from '../../shared/graphs.utils'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| import { PriceService, Price } from '../../services/price.service'; | ||||
| import { CacheService } from '../../services/cache.service'; | ||||
| 
 | ||||
| @ -206,6 +207,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|                       this.error = err; | ||||
|                       this.isLoadingBlock = false; | ||||
|                       this.isLoadingOverview = false; | ||||
|                       this.seoService.logSoft404(); | ||||
|                       return EMPTY; | ||||
|                     }) | ||||
|                   ); | ||||
| @ -214,6 +216,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|                   this.error = err; | ||||
|                   this.isLoadingBlock = false; | ||||
|                   this.isLoadingOverview = false; | ||||
|                   this.seoService.logSoft404(); | ||||
|                   return EMPTY; | ||||
|                 }), | ||||
|               ); | ||||
| @ -229,6 +232,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|               this.error = err; | ||||
|               this.isLoadingBlock = false; | ||||
|               this.isLoadingOverview = false; | ||||
|               this.seoService.logSoft404(); | ||||
|               return EMPTY; | ||||
|             }) | ||||
|           ); | ||||
| @ -258,6 +262,11 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         this.setNextAndPreviousBlockLink(); | ||||
| 
 | ||||
|         this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`); | ||||
|         if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) { | ||||
|           this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||
|         } else { | ||||
|           this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||
|         } | ||||
|         this.isLoadingBlock = false; | ||||
|         this.setBlockSubsidy(); | ||||
|         if (block?.extras?.reward !== undefined) { | ||||
| @ -322,7 +331,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|         ]); | ||||
|       }) | ||||
|     ) | ||||
|     .subscribe(([transactions, blockAudit]) => {       | ||||
|     .subscribe(([transactions, blockAudit]) => { | ||||
|       if (transactions) { | ||||
|         this.strippedTransactions = transactions; | ||||
|       } else { | ||||
| @ -340,12 +349,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 +378,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 +405,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 +424,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; | ||||
| @ -664,7 +686,7 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|       this.setAuditAvailable(false); | ||||
|     } | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   isAuditAvailableFromBlockHeight(blockHeight: number): boolean { | ||||
|     if (!this.auditSupported) { | ||||
|       return false; | ||||
| @ -713,4 +735,4 @@ export class BlockComponent implements OnInit, OnDestroy { | ||||
|       this.block.canonical = block.id; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core'; | ||||
| import { firstValueFrom, Subscription } from 'rxjs'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| 
 | ||||
| @ -8,12 +8,13 @@ import { StateService } from '../../services/state.service'; | ||||
|   styleUrls: ['./blockchain.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class BlockchainComponent implements OnInit, OnDestroy { | ||||
| export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { | ||||
|   @Input() pages: any[] = []; | ||||
|   @Input() pageIndex: number; | ||||
|   @Input() blocksPerPage: number = 8; | ||||
|   @Input() minScrollWidth: number = 0; | ||||
|   @Input() scrollableMempool: boolean = false; | ||||
|   @Input() containerWidth: number; | ||||
| 
 | ||||
|   @Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter(); | ||||
| 
 | ||||
| @ -85,19 +86,25 @@ export class BlockchainComponent implements OnInit, OnDestroy { | ||||
|     this.mempoolOffsetChange.emit(this.mempoolOffset); | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.containerWidth) { | ||||
|       this.onResize(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onResize(): void { | ||||
|     if (window.innerWidth >= 768) { | ||||
|     const width = this.containerWidth || window.innerWidth; | ||||
|     if (width >= 768) { | ||||
|       if (this.stateService.isLiquid()) { | ||||
|         this.dividerOffset = 420; | ||||
|       } else { | ||||
|         this.dividerOffset = window.innerWidth * 0.5; | ||||
|         this.dividerOffset = width * 0.5; | ||||
|       } | ||||
|     } else { | ||||
|       if (this.stateService.isLiquid()) { | ||||
|         this.dividerOffset = window.innerWidth * 0.5; | ||||
|         this.dividerOffset = width * 0.5; | ||||
|       } else { | ||||
|         this.dividerOffset = window.innerWidth * 0.95; | ||||
|         this.dividerOffset = width * 0.95; | ||||
|       } | ||||
|     } | ||||
|     this.cd.markForCheck(); | ||||
|  | ||||
| @ -5,6 +5,8 @@ import { BlockExtended } from '../../interfaces/node-api.interface'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { WebsocketService } from '../../services/websocket.service'; | ||||
| import { SeoService } from '../../services/seo.service'; | ||||
| import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-blocks-list', | ||||
| @ -36,6 +38,7 @@ export class BlocksList implements OnInit { | ||||
|     private websocketService: WebsocketService, | ||||
|     public stateService: StateService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     private seoService: SeoService, | ||||
|   ) { | ||||
|     this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool'; | ||||
|   } | ||||
| @ -52,6 +55,14 @@ export class BlocksList implements OnInit { | ||||
|     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; | ||||
|     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||
| 
 | ||||
|     this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`); | ||||
|     if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) { | ||||
|       this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`); | ||||
|     } else { | ||||
|       this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     this.blocks$ = combineLatest([ | ||||
|       this.fromHeightSubject.pipe( | ||||
|         switchMap((fromBlockHeight) => { | ||||
|  | ||||
| @ -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> | ||||
| </div> | ||||
| <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'" | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user