Merge branch 'master' into nymkappa/health-check
This commit is contained in:
		
						commit
						cef6127e69
					
				
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -35,7 +35,7 @@ jobs: | |||||||
|       - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain |       - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain | ||||||
|         # Latest version available on this commit is 1.71.1 |         # Latest version available on this commit is 1.71.1 | ||||||
|         # Commit date is Aug 3, 2023 |         # Commit date is Aug 3, 2023 | ||||||
|         uses: dtolnay/rust-toolchain@dc6353516c68da0f06325f42ad880f76a5e77ec9 |         uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a | ||||||
|         with: |         with: | ||||||
|           toolchain: ${{ steps.gettoolchain.outputs.toolchain }} |           toolchain: ${{ steps.gettoolchain.outputs.toolchain }} | ||||||
| 
 | 
 | ||||||
| @ -257,7 +257,7 @@ jobs: | |||||||
|             spec: | |             spec: | | ||||||
|               cypress/e2e/mainnet/*.spec.ts |               cypress/e2e/mainnet/*.spec.ts | ||||||
|               cypress/e2e/signet/*.spec.ts |               cypress/e2e/signet/*.spec.ts | ||||||
|               cypress/e2e/testnet/*.spec.ts |               cypress/e2e/testnet4/*.spec.ts | ||||||
|           - module: "liquid" |           - module: "liquid" | ||||||
|             spec: | |             spec: | | ||||||
|               cypress/e2e/liquid/liquid.spec.ts |               cypress/e2e/liquid/liquid.spec.ts | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| The Mempool Open Source Project® | The Mempool Open Source Project® | ||||||
| Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders | Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders | ||||||
| 
 | 
 | ||||||
| This program is free software; you can redistribute it and/or modify it under | This program is free software; you can redistribute it and/or modify it under | ||||||
| the terms of the GNU Affero General Public License as published by the Free | the terms of the GNU Affero General Public License as published by the Free | ||||||
| @ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project. | |||||||
| 
 | 
 | ||||||
| The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,  | The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,  | ||||||
| Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full  | Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full  | ||||||
| Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,  | Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,  | ||||||
| the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical  | the mempool block visualization Logo, the mempool Blocks Logo, the mempool  | ||||||
| Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks  | transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,  | ||||||
| of Mempool Space K.K in Japan, the United States, and/or other countries. | the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are  | ||||||
|  | registered trademarks or trademarks of Mempool Space K.K in Japan,  | ||||||
|  | the United States, and/or other countries. | ||||||
| 
 | 
 | ||||||
| See our full Trademark Policy and Guidelines for more details, published on  | See our full Trademark Policy and Guidelines for more details, published on  | ||||||
| <https://mempool.space/trademark-policy>. | <https://mempool.space/trademark-policy>. | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ | |||||||
|     "@typescript-eslint/no-this-alias": 1, |     "@typescript-eslint/no-this-alias": 1, | ||||||
|     "@typescript-eslint/no-var-requires": 1, |     "@typescript-eslint/no-var-requires": 1, | ||||||
|     "@typescript-eslint/explicit-function-return-type": 1, |     "@typescript-eslint/explicit-function-return-type": 1, | ||||||
|  |     "@typescript-eslint/no-unused-vars": 1, | ||||||
|     "no-console": 1, |     "no-console": 1, | ||||||
|     "no-constant-condition": 1, |     "no-constant-condition": 1, | ||||||
|     "no-dupe-else-if": 1, |     "no-dupe-else-if": 1, | ||||||
| @ -32,6 +33,8 @@ | |||||||
|     "prefer-rest-params": 1, |     "prefer-rest-params": 1, | ||||||
|     "quotes": [1, "single", { "allowTemplateLiterals": true }], |     "quotes": [1, "single", { "allowTemplateLiterals": true }], | ||||||
|     "semi": 1, |     "semi": 1, | ||||||
|     "eqeqeq": 1 |     "curly": [1, "all"], | ||||||
|  |     "eqeqeq": 1, | ||||||
|  |     "no-trailing-spaces": 1 | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec) | |||||||
| 
 | 
 | ||||||
| #### Build | #### Build | ||||||
| 
 | 
 | ||||||
| _Make sure to use Node.js 16.10 and npm 7._ | _Make sure to use Node.js 20.x and npm 9.x or newer_ | ||||||
| 
 | 
 | ||||||
| _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._ | _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._ | ||||||
| 
 | 
 | ||||||
| @ -181,7 +181,7 @@ Create a new wallet, if needed: | |||||||
|    bitcoin-cli -regtest createwallet test |    bitcoin-cli -regtest createwallet test | ||||||
|    ``` |    ``` | ||||||
| 
 | 
 | ||||||
| Load wallet (this command may take a while if you have lot of UTXOs): | Load wallet (this command may take a while if you have a lot of UTXOs): | ||||||
|    ``` |    ``` | ||||||
|    bitcoin-cli -regtest loadwallet test |    bitcoin-cli -regtest loadwallet test | ||||||
|    ``` |    ``` | ||||||
| @ -229,13 +229,13 @@ Generate block at regular interval (every 10 seconds in this example): | |||||||
| 
 | 
 | ||||||
| ### Mining pools update | ### Mining pools update | ||||||
| 
 | 
 | ||||||
| By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).  | By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` is set to `false`).  | ||||||
| 
 | 
 | ||||||
| To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks. | To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks. | ||||||
| 
 | 
 | ||||||
| You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`. | You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_POOLS_UPDATE` to `true` in your `mempool-config.json`. | ||||||
| 
 | 
 | ||||||
| When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed. | When a `coinbase tag` or `coinbase address` change is detected, pool assignments for all relevant blocks (tagged to that pool or the `unknown` mining pool, starting from height 130635) are updated using the new criteria. | ||||||
| 
 | 
 | ||||||
| ### Re-index tables | ### Re-index tables | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ | |||||||
|     "EXTERNAL_RETRY_INTERVAL": 0, |     "EXTERNAL_RETRY_INTERVAL": 0, | ||||||
|     "USER_AGENT": "mempool", |     "USER_AGENT": "mempool", | ||||||
|     "STDOUT_LOG_MIN_PRIORITY": "debug", |     "STDOUT_LOG_MIN_PRIORITY": "debug", | ||||||
|     "AUTOMATIC_BLOCK_REINDEXING": false, |     "AUTOMATIC_POOLS_UPDATE": false, | ||||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", |     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", | ||||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", |     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||||
|     "AUDIT": false, |     "AUDIT": false, | ||||||
| @ -35,7 +35,8 @@ | |||||||
|     "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, |     "MAX_PUSH_TX_SIZE_WEIGHT": 4000000, | ||||||
|     "ALLOW_UNREACHABLE": true, |     "ALLOW_UNREACHABLE": true, | ||||||
|     "PRICE_UPDATES_PER_HOUR": 1, |     "PRICE_UPDATES_PER_HOUR": 1, | ||||||
|     "MAX_TRACKED_ADDRESSES": 100 |     "MAX_TRACKED_ADDRESSES": 100, | ||||||
|  |     "UNIX_SOCKET_PATH": "" | ||||||
|   }, |   }, | ||||||
|   "CORE_RPC": { |   "CORE_RPC": { | ||||||
|     "HOST": "127.0.0.1", |     "HOST": "127.0.0.1", | ||||||
| @ -58,7 +59,8 @@ | |||||||
|     "RETRY_UNIX_SOCKET_AFTER": 30000, |     "RETRY_UNIX_SOCKET_AFTER": 30000, | ||||||
|     "REQUEST_TIMEOUT": 10000, |     "REQUEST_TIMEOUT": 10000, | ||||||
|     "FALLBACK_TIMEOUT": 5000, |     "FALLBACK_TIMEOUT": 5000, | ||||||
|     "FALLBACK": [] |     "FALLBACK": [], | ||||||
|  |     "MAX_BEHIND_TIP": 2 | ||||||
|   }, |   }, | ||||||
|   "SECOND_CORE_RPC": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "127.0.0.1", |     "HOST": "127.0.0.1", | ||||||
| @ -138,6 +140,8 @@ | |||||||
|     "ENABLED": false, |     "ENABLED": false, | ||||||
|     "AUDIT": false, |     "AUDIT": false, | ||||||
|     "AUDIT_START_HEIGHT": 774000, |     "AUDIT_START_HEIGHT": 774000, | ||||||
|  |     "STATISTICS": false, | ||||||
|  |     "STATISTICS_START_TIME": 1481932800, | ||||||
|     "SERVERS": [ |     "SERVERS": [ | ||||||
|       "list", |       "list", | ||||||
|       "of", |       "of", | ||||||
| @ -151,6 +155,7 @@ | |||||||
|   }, |   }, | ||||||
|   "FIAT_PRICE": { |   "FIAT_PRICE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  |     "PAID": false, | ||||||
|     "API_KEY": "your-api-key-from-freecurrencyapi.com" |     "API_KEY": "your-api-key-from-freecurrencyapi.com" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										767
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										767
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "mempool-backend", |   "name": "mempool-backend", | ||||||
|   "version": "3.0.0-dev", |   "version": "3.1.0-dev", | ||||||
|   "description": "Bitcoin mempool visualizer and blockchain explorer backend", |   "description": "Bitcoin mempool visualizer and blockchain explorer backend", | ||||||
|   "license": "GNU Affero General Public License v3.0", |   "license": "GNU Affero General Public License v3.0", | ||||||
|   "homepage": "https://mempool.space", |   "homepage": "https://mempool.space", | ||||||
| @ -39,24 +39,24 @@ | |||||||
|     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" |     "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@babel/core": "^7.24.0", |     "@babel/core": "^7.25.2", | ||||||
|     "@mempool/electrum-client": "1.1.9", |     "@mempool/electrum-client": "1.1.9", | ||||||
|     "@types/node": "^18.15.3", |     "@types/node": "^18.15.3", | ||||||
|     "axios": "~1.6.1", |     "axios": "~1.7.4", | ||||||
|     "bitcoinjs-lib": "~6.1.3", |     "bitcoinjs-lib": "~6.1.3", | ||||||
|     "crypto-js": "~4.2.0", |     "crypto-js": "~4.2.0", | ||||||
|     "express": "~4.19.2", |     "express": "~4.19.2", | ||||||
|     "maxmind": "~4.3.11", |     "maxmind": "~4.3.11", | ||||||
|     "mysql2": "~3.9.1", |     "mysql2": "~3.11.0", | ||||||
|     "rust-gbt": "file:./rust-gbt", |     "rust-gbt": "file:./rust-gbt", | ||||||
|     "redis": "^4.6.6", |     "redis": "^4.7.0", | ||||||
|     "socks-proxy-agent": "~7.0.0", |     "socks-proxy-agent": "~7.0.0", | ||||||
|     "typescript": "~4.9.3", |     "typescript": "~4.9.3", | ||||||
|     "ws": "~8.16.0" |     "ws": "~8.18.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/code-frame": "^7.18.6", |     "@babel/code-frame": "^7.18.6", | ||||||
|     "@babel/core": "^7.24.0", |     "@babel/core": "^7.25.2", | ||||||
|     "@types/compression": "^1.7.2", |     "@types/compression": "^1.7.2", | ||||||
|     "@types/crypto-js": "^4.1.1", |     "@types/crypto-js": "^4.1.1", | ||||||
|     "@types/express": "^4.17.17", |     "@types/express": "^4.17.17", | ||||||
|  | |||||||
| @ -7,9 +7,10 @@ | |||||||
|     "BLOCKS_SUMMARIES_INDEXING": true, |     "BLOCKS_SUMMARIES_INDEXING": true, | ||||||
|     "GOGGLES_INDEXING": false, |     "GOGGLES_INDEXING": false, | ||||||
|     "HTTP_PORT": 1, |     "HTTP_PORT": 1, | ||||||
|  |     "UNIX_SOCKET_PATH": "/mempool/socket/mempool-bitcoin-mainnet", | ||||||
|     "SPAWN_CLUSTER_PROCS": 2, |     "SPAWN_CLUSTER_PROCS": 2, | ||||||
|     "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__", |     "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__", | ||||||
|     "AUTOMATIC_BLOCK_REINDEXING": false, |     "AUTOMATIC_POOLS_UPDATE": false, | ||||||
|     "POLL_RATE_MS": 3, |     "POLL_RATE_MS": 3, | ||||||
|     "CACHE_DIR": "__MEMPOOL_CACHE_DIR__", |     "CACHE_DIR": "__MEMPOOL_CACHE_DIR__", | ||||||
|     "CACHE_ENABLED": true, |     "CACHE_ENABLED": true, | ||||||
| @ -59,7 +60,8 @@ | |||||||
|     "RETRY_UNIX_SOCKET_AFTER": 888, |     "RETRY_UNIX_SOCKET_AFTER": 888, | ||||||
|     "REQUEST_TIMEOUT": 10000, |     "REQUEST_TIMEOUT": 10000, | ||||||
|     "FALLBACK_TIMEOUT": 5000, |     "FALLBACK_TIMEOUT": 5000, | ||||||
|     "FALLBACK": [] |     "FALLBACK": [], | ||||||
|  |     "MAX_BEHIND_TIP": 2 | ||||||
|   }, |   }, | ||||||
|   "SECOND_CORE_RPC": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", |     "HOST": "__SECOND_CORE_RPC_HOST__", | ||||||
| @ -130,6 +132,8 @@ | |||||||
|     "ENABLED": false, |     "ENABLED": false, | ||||||
|     "AUDIT": false, |     "AUDIT": false, | ||||||
|     "AUDIT_START_HEIGHT": 774000, |     "AUDIT_START_HEIGHT": 774000, | ||||||
|  |     "STATISTICS": false, | ||||||
|  |     "STATISTICS_START_TIME": 1481932800, | ||||||
|     "SERVERS": [] |     "SERVERS": [] | ||||||
|   }, |   }, | ||||||
|   "MEMPOOL_SERVICES": { |   "MEMPOOL_SERVICES": { | ||||||
| @ -143,6 +147,7 @@ | |||||||
|   }, |   }, | ||||||
|   "FIAT_PRICE": { |   "FIAT_PRICE": { | ||||||
|     "ENABLED": true, |     "ENABLED": true, | ||||||
|  |     "PAID": false, | ||||||
|     "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" |     "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,21 +4,37 @@ import { MempoolTransactionExtended } from '../../mempool.interfaces'; | |||||||
| const randomTransactions = require('./test-data/transactions-random.json'); | const randomTransactions = require('./test-data/transactions-random.json'); | ||||||
| const replacedTransactions = require('./test-data/transactions-replaced.json'); | const replacedTransactions = require('./test-data/transactions-replaced.json'); | ||||||
| const rbfTransactions = require('./test-data/transactions-rbfs.json'); | const rbfTransactions = require('./test-data/transactions-rbfs.json'); | ||||||
|  | const nonStandardTransactions = require('./test-data/non-standard-txs.json'); | ||||||
| 
 | 
 | ||||||
| describe('Mempool Utils', () => { | describe('Common', () => { | ||||||
|   test('should detect RBF transactions with fast method', () => { |   describe('RBF', () => { | ||||||
|     const newTransactions = rbfTransactions.concat(randomTransactions); |     const newTransactions = rbfTransactions.concat(randomTransactions); | ||||||
|     const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); |     test('should detect RBF transactions with fast method', () => { | ||||||
|     expect(Object.values(result).length).toEqual(2); |       const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); | ||||||
|     expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); |       expect(Object.values(result).length).toEqual(2); | ||||||
|     expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); |       expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); | ||||||
|  |       expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     test('should detect RBF transactions with scalable method', () => { | ||||||
|  |       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'); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test.only('should detect RBF transactions with scalable method', () => { |   describe('Mempool Goggles', () => { | ||||||
|     const newTransactions = rbfTransactions.concat(randomTransactions); |     test('should detect nonstandard transactions', () => { | ||||||
|     const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); |       nonStandardTransactions.forEach((tx) => { | ||||||
|     expect(Object.values(result).length).toEqual(2); |         expect(Common.isNonStandard(tx)).toEqual(true); | ||||||
|     expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); |       }); | ||||||
|     expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); |     }); | ||||||
|  |    | ||||||
|  |     test('should not misclassify as nonstandard transactions', () => { | ||||||
|  |       randomTransactions.forEach((tx) => { | ||||||
|  |         expect(Common.isNonStandard(tx)).toEqual(false); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								backend/src/__tests__/api/test-data/non-standard-txs.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								backend/src/__tests__/api/test-data/non-standard-txs.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | [ | ||||||
|  |   { | ||||||
|  |     "txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367", | ||||||
|  |     "version": 1, | ||||||
|  |     "locktime": 0, | ||||||
|  |     "vin": [ | ||||||
|  |         { | ||||||
|  |             "txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019", | ||||||
|  |             "vout": 4217, | ||||||
|  |             "prevout": { | ||||||
|  |                 "scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3", | ||||||
|  |                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3", | ||||||
|  |                 "scriptpubkey_type": "v0_p2wpkh", | ||||||
|  |                 "scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33", | ||||||
|  |                 "value": 106 | ||||||
|  |             }, | ||||||
|  |             "scriptsig": "", | ||||||
|  |             "scriptsig_asm": "", | ||||||
|  |             "witness": [ | ||||||
|  |                 "3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01", | ||||||
|  |                 "02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d" | ||||||
|  |             ], | ||||||
|  |             "is_coinbase": false, | ||||||
|  |             "sequence": 4294967295 | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "vout": [ | ||||||
|  |         { | ||||||
|  |             "scriptpubkey": "6a023a29", | ||||||
|  |             "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29", | ||||||
|  |             "scriptpubkey_type": "op_return", | ||||||
|  |             "value": 0 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "scriptpubkey": "6a036d7648", | ||||||
|  |             "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648", | ||||||
|  |             "scriptpubkey_type": "op_return", | ||||||
|  |             "value": 0 | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "size": 186, | ||||||
|  |     "weight": 420, | ||||||
|  |     "sigops": 1, | ||||||
|  |     "fee": 106, | ||||||
|  |     "status": { | ||||||
|  |         "confirmed": true, | ||||||
|  |         "block_height": 836361, | ||||||
|  |         "block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b", | ||||||
|  |         "block_time": 1711448028 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | ] | ||||||
| @ -273,5 +273,328 @@ | |||||||
|         }, |         }, | ||||||
|         "bestDescendant": null, |         "bestDescendant": null, | ||||||
|         "cpfpChecked": true |         "cpfpChecked": true | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "txid": "20b984492b5264162a4c92c9a34bc7fa08b67d669de7b4c5982ad3cb28aaecf6", | ||||||
|  |         "version": 2, | ||||||
|  |         "locktime": 0, | ||||||
|  |         "vin": [ | ||||||
|  |             { | ||||||
|  |                 "txid": "3adda6afd547193793c248e667c2b7dbf26d705003de65e3a25e5be698286aef", | ||||||
|  |                 "vout": 2, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644", | ||||||
|  |                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644", | ||||||
|  |                     "scriptpubkey_type": "v0_p2wpkh", | ||||||
|  |                     "scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6", | ||||||
|  |                     "value": 27619 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "", | ||||||
|  |                 "scriptsig_asm": "", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "304402205d7f1e0d928982645c2bcc4c730c4545c382d6520c2a14eebc71594702cd06b302200511d452ce51c79017536f50acb115eefe7c04506ad12b9307d2b5d56b999beb01", | ||||||
|  |                     "03716cb4f0430fe69c596a12c6680c55803150645989b406772838d548cde7cca5" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 4294967295 | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         "vout": [ | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "6a5d0614c0a2331441", | ||||||
|  |                 "scriptpubkey_asm": "OP_RETURN OP_PUSHNUM_13 OP_PUSHBYTES_6 14c0a2331441", | ||||||
|  |                 "scriptpubkey_type": "op_return", | ||||||
|  |                 "value": 0 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "5114d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c", | ||||||
|  |                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_20 d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c", | ||||||
|  |                 "scriptpubkey_type": "unknown", | ||||||
|  |                 "scriptpubkey_address": "bc1p6uwxc048hflxaerh5zlastpqc7ye0zpvq7gq2a", | ||||||
|  |                 "value": 546 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644", | ||||||
|  |                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644", | ||||||
|  |                 "scriptpubkey_type": "v0_p2wpkh", | ||||||
|  |                 "scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6", | ||||||
|  |                 "value": 23073 | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         "size": 240, | ||||||
|  |         "weight": 633, | ||||||
|  |         "sigops": 1, | ||||||
|  |         "fee": 4000, | ||||||
|  |         "status": { | ||||||
|  |             "confirmed": true, | ||||||
|  |             "block_height": 848136, | ||||||
|  |             "block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d", | ||||||
|  |             "block_time": 1718517071 | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "txid": "b10c0000004da5a9d1d9b4ae32e09f0b3e62d21a5cce5428d4ad714fb444eb5d", | ||||||
|  |         "version": 1, | ||||||
|  |         "locktime": 1231006505, | ||||||
|  |         "vin": [ | ||||||
|  |             { | ||||||
|  |                 "txid": "d46a24962c1d7bd6e87d80570c6a53413eaf30d7fde7f52347f13645ae53969b", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "41049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cfac", | ||||||
|  |                     "scriptpubkey_asm": "OP_PUSHBYTES_65 049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cf OP_CHECKSIG", | ||||||
|  |                     "scriptpubkey_type": "p2pk", | ||||||
|  |                     "value": 6102 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "473044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601", | ||||||
|  |                 "scriptsig_asm": "OP_PUSHBYTES_71 3044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601", | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 20090103 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "76a914bbb1f7d0f7e15ac088af9bafe25aaac1a59832d088ac", | ||||||
|  |                     "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 bbb1f7d0f7e15ac088af9bafe25aaac1a59832d0 OP_EQUALVERIFY OP_CHECKSIG", | ||||||
|  |                     "scriptpubkey_type": "p2pkh", | ||||||
|  |                     "scriptpubkey_address": "1J7SZJry7CX4zWdH3P8E8UJjZrhcLEjJ39", | ||||||
|  |                     "value": 1913 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "46304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad8510221028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40", | ||||||
|  |                 "scriptsig_asm": "OP_PUSHBYTES_70 304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad85102 OP_PUSHBYTES_33 028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40", | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 20081031 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||||
|  |                 "vout": 1, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "52210304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f2102b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f53ae", | ||||||
|  |                     "scriptpubkey_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 0304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f OP_PUSHBYTES_33 02b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_PUSHNUM_3 OP_CHECKMULTISIG", | ||||||
|  |                     "scriptpubkey_type": "multisig", | ||||||
|  |                     "value": 1971 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "00453042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e2033b303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481", | ||||||
|  |                 "scriptsig_asm": "OP_0 OP_PUSHBYTES_69 3042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e203 OP_PUSHBYTES_59 303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481", | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 19750504 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "45e1cb33599acb071810ccc801b71bd7610865f5b899492946ab1bfbcb61cad6", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "a91419f0b86f61606c6eb51b217698ca7e8bff1e398b87", | ||||||
|  |                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 19f0b86f61606c6eb51b217698ca7e8bff1e398b OP_EQUAL", | ||||||
|  |                     "scriptpubkey_type": "p2sh", | ||||||
|  |                     "scriptpubkey_address": "344BBtYkhaCXgA7oYSXASUfh4bFieiponG", | ||||||
|  |                     "value": 2140 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "00443041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea013a303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef8239303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba834ced532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae", | ||||||
|  |                 "scriptsig_asm": "OP_0 OP_PUSHBYTES_68 3041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea01 OP_PUSHBYTES_58 303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef82 OP_PUSHBYTES_57 303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba83 OP_PUSHDATA1 532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae", | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 16, | ||||||
|  |                 "inner_redeemscript_asm": "OP_PUSHNUM_3 OP_PUSHBYTES_33 03e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc OP_PUSHBYTES_33 03cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd4 OP_PUSHBYTES_33 027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906 OP_PUSHBYTES_65 0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_PUSHBYTES_65 04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_PUSHNUM_5 OP_CHECKMULTISIG" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||||
|  |                 "vout": 2, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "a9143b13a1f71c20c799d86bb624b3898c826d6c82da87", | ||||||
|  |                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 3b13a1f71c20c799d86bb624b3898c826d6c82da OP_EQUAL", | ||||||
|  |                     "scriptpubkey_type": "p2sh", | ||||||
|  |                     "scriptpubkey_address": "375PJxsKRtAq4WoS6u82jvgZW94R8Wx3iH", | ||||||
|  |                     "value": 5139 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "1600149b27f072e4b972927c445d1946162a550b0914d8", | ||||||
|  |                 "scriptsig_asm": "OP_PUSHBYTES_22 00149b27f072e4b972927c445d1946162a550b0914d8", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "3040021c23902a01d4c5cff2c33c8bdb778a5aadea78a9a0d6d4db60aaa0fba1022069237d9dbf2db8cff9c260ba71250493682d01a746f4a45c5c7ea386e56d2bc902", | ||||||
|  |                     "0240187acd3e2fd3d8e1acffefa85907b6550730c24f78dfd3301c829fc4daf3cc" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 141, | ||||||
|  |                 "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_20 9b27f072e4b972927c445d1946162a550b0914d8" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||||
|  |                 "vout": 3, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "a914a3c0698f2300c7b2e8107d4c9c988e642110039087", | ||||||
|  |                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3c0698f2300c7b2e8107d4c9c988e6421100390 OP_EQUAL", | ||||||
|  |                     "scriptpubkey_type": "p2sh", | ||||||
|  |                     "scriptpubkey_address": "3GcrZrbUuvE4UtUdSbKTXcRnTqmfMdyMAC", | ||||||
|  |                     "value": 3220 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "220020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||||
|  |                 "scriptsig_asm": "OP_PUSHBYTES_34 0020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "303f021c65aee6696e80be6e14545cfd64b44f17b0514c150eefdb090c0f0bd9021f3fef4aa95c252a225622aba99e4d5af5a6fe40d177acd593e64cf2f8557ccc03", | ||||||
|  |                     "03b55c6f0749e0f3e2caeca05f68e3699f1b3c62a550730f704985a6a9aae437a1", | ||||||
|  |                     "76a914db865fd920959506111079995f1e4017b489bfe38763ac6721024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c7c820120876475527c2103443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca52ae67a91446c3747322b220fdb925c9802f0e949c1feab99988ac6868" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 3735928559, | ||||||
|  |                 "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||||
|  |                 "inner_witnessscript_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 db865fd920959506111079995f1e4017b489bfe3 OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 03443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 46c3747322b220fdb925c9802f0e949c1feab999 OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF OP_ENDIF" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||||
|  |                 "vout": 4, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "0014c0ca6e754e65d3ba59112d7abc33e500c00ecfa7", | ||||||
|  |                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c0ca6e754e65d3ba59112d7abc33e500c00ecfa7", | ||||||
|  |                     "scriptpubkey_type": "v0_p2wpkh", | ||||||
|  |                     "scriptpubkey_address": "bc1qcr9xua2wvhfm5kg394atcvl9qrqqana8rrmy8h", | ||||||
|  |                     "value": 17144 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "", | ||||||
|  |                 "scriptsig_asm": "", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "303e021c11f60486afd0f5d6573603fb2076ef2f676455b92ada257d2f25558a021e317719c946f951d49bf4df4285a618629cd9e554fcbf787c319a0c4dd22601", | ||||||
|  |                     "032467f24cc31664f0cf34ff8d5cbb590888ddc1dcfec724a32ae3dd5338b8508e" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 21000000 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "637db3928a8fb1b22b81f92dc738ee7637e5b172d650363d0b327429578bd001", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "0020a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357", | ||||||
|  |                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357", | ||||||
|  |                     "scriptpubkey_type": "v0_p2wsh", | ||||||
|  |                     "scriptpubkey_address": "bc1q49fs59nletdxwtq59mnrdhx3w9uku6003cm658mh7dw93mwh5dts2w2kht", | ||||||
|  |                     "value": 8149 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "", | ||||||
|  |                 "scriptsig_asm": "", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "303d021c32f9454db85cb1a4ca63a9883d4347c5e13f3654e884ae44e9efa3c8021d62f07fe452c06b084bc3e09afd3aac4039136549a465533bc1ca66967902", | ||||||
|  |                     "01", | ||||||
|  |                     "632102fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd67012ab27521034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f68ac" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 4190024921, | ||||||
|  |                 "inner_witnessscript_asm": "OP_IF OP_PUSHBYTES_33 02fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd OP_ELSE OP_PUSHBYTES_1 2a OP_CSV OP_DROP OP_PUSHBYTES_33 034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f OP_ENDIF OP_CHECKSIG" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "0020db02df125062ebae5bacd189ebff22577b2817c1872be79a0d3ba3982c41", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "512071212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849", | ||||||
|  |                     "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 71212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849", | ||||||
|  |                     "scriptpubkey_type": "v1_p2tr", | ||||||
|  |                     "scriptpubkey_address": "bc1pwysjmmg07nymrvx9qhvqzfmjutd7nz3u4ecksdmmj58mdwrx4pysq6m68g", | ||||||
|  |                     "value": 9001 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "", | ||||||
|  |                 "scriptsig_asm": "", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "d822f203827852998cad370232e8c57294540a5da51107fa26cf466bdd2b8b0b3d161999cc80aed8de7386a2bd5d5313aea159a231cc26fa53aaa702b7fa21ed" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 341 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "txid": "795741ecf9c431b14b1c8d2dd017d3978fd4f6452e91edf416f31ef9971206b4", | ||||||
|  |                 "vout": 0, | ||||||
|  |                 "prevout": { | ||||||
|  |                     "scriptpubkey": "512089ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a", | ||||||
|  |                     "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 89ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a", | ||||||
|  |                     "scriptpubkey_type": "v1_p2tr", | ||||||
|  |                     "scriptpubkey_address": "bc1p3xkpyzjfpmhg3k643qgjl90cszfjsnypfuru8tv58fl6a7azyudqkcu66k", | ||||||
|  |                     "value": 19953 | ||||||
|  |                 }, | ||||||
|  |                 "scriptsig": "", | ||||||
|  |                 "scriptsig_asm": "", | ||||||
|  |                 "witness": [ | ||||||
|  |                     "fe6eb715dceffefc067fdc787d250a9a9116682d216f6356ea38fc1f112bd74995faa90315e81981d2c2260b7eaca3c41a16b280362980f0d8faf4c05ebb82c5", | ||||||
|  |                     "e34ad0ad33885a473831f8ba8d9339123cb19d0e642e156d8e0d6e2ab2691aedb30e55a35637a806927225e1aa72223d41e59f92c6579b819e7d331a7ada9d2e01", | ||||||
|  |                     "2a4861fb4cb951c791bf6c93859ef65abccd90034f91b9b77abb918e13b6fce75d5fa3e2d2f6eeeae105315178c2cb9db2ef238fe89b282f691c06db43bc71ca02", | ||||||
|  |                     "fc97bb2be673c3bf388aaf58178ef14d354caf83c92aca8ef1831d619b8511e928f4f5fdea3962067b11e7cecfe094cd0f66a4ea9af9ec836d70d18f2b37df0281", | ||||||
|  |                     "a5781a0adaa80ab7f7f164172dd1a1cb127e523daa0d6949aba074a15c589f12dfb8183182afec9230cb7947b7422a4abc1bb78173550d66274ea19f6c9dd92c82", | ||||||
|  |                     "", | ||||||
|  |                     "", | ||||||
|  |                     "205f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1ac205f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3ba205ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996ba20b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690ba20d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5ba20cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0ba20aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545ba559c", | ||||||
|  |                     "c0b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f5534a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e166f7cf9580f1c2dfb3c4d5d043cdbb128c640e3f20161245aa7372e9666168516a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48dd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb46829a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713d29c9c0e8e4d2a9790922af73f0b8d51f0bd4bb19940d9cf910ead8fbe85bc9bbb41a757f405890fb0f5856228e23b715702d714d59bf2b1feb70d8b2b4e3e089fdbcf0ef9d8d00f66e47917f67cc5d78aec1ac786e2abb8d2facb4e4790aad6cc455ae816e6cdafdb58d54e35d4f46d860047458eacf1c7405dc634631c570d8d31992805518fd62daa3bdd2a5c4fd2cd3054c9b3dca1d78055e9528cff6adc8f907925d2ebe48765103e6845c06f1f2bb77c6adc1cc002865865eb5cfd5c1cb10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d4133e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac9879903637777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8fd456524104a6674693c29946543f8a0befccce5a352bda55ec8559fc630f5f37393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef7373be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7eb5a2af5845a8d3796308ff9840e567b14cf6bb158ff26c999e6f9a1f5448f9aa" | ||||||
|  |                 ], | ||||||
|  |                 "is_coinbase": false, | ||||||
|  |                 "sequence": 342, | ||||||
|  |                 "inner_witnessscript_asm": "OP_PUSHBYTES_32 5f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1 OP_CHECKSIG OP_PUSHBYTES_32 5f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3 OP_CHECKSIGADD OP_PUSHBYTES_32 5ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996 OP_CHECKSIGADD OP_PUSHBYTES_32 b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690 OP_CHECKSIGADD OP_PUSHBYTES_32 d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5 OP_CHECKSIGADD OP_PUSHBYTES_32 cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0 OP_CHECKSIGADD OP_PUSHBYTES_32 aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545 OP_CHECKSIGADD OP_PUSHNUM_5 OP_NUMEQUAL" | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         "vout": [ | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "210261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32aac", | ||||||
|  |                 "scriptpubkey_asm": "OP_PUSHBYTES_33 0261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32a OP_CHECKSIG", | ||||||
|  |                 "scriptpubkey_type": "p2pk", | ||||||
|  |                 "value": 576 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "76a9140240539af6c68431e4ce9cc5ef464f12c1741b3c88ac", | ||||||
|  |                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 0240539af6c68431e4ce9cc5ef464f12c1741b3c OP_EQUALVERIFY OP_CHECKSIG", | ||||||
|  |                 "scriptpubkey_type": "p2pkh", | ||||||
|  |                 "scriptpubkey_address": "1CuQsdrcgcmPvugo3NqEwh1kDcpeEnuFC", | ||||||
|  |                 "value": 546 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "5121028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae2851ae", | ||||||
|  |                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_33 028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae28 OP_PUSHNUM_1 OP_CHECKMULTISIG", | ||||||
|  |                 "scriptpubkey_type": "multisig", | ||||||
|  |                 "value": 582 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "a91449ed2c96e33b6134408af8484508bcc3248c8dbd87", | ||||||
|  |                 "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 49ed2c96e33b6134408af8484508bcc3248c8dbd OP_EQUAL", | ||||||
|  |                 "scriptpubkey_type": "p2sh", | ||||||
|  |                 "scriptpubkey_address": "38RuNhSiZiftB6WVnStu5aUz6jXtCDXQZk", | ||||||
|  |                 "value": 540 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "0014c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba", | ||||||
|  |                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba", | ||||||
|  |                 "scriptpubkey_type": "v0_p2wpkh", | ||||||
|  |                 "scriptpubkey_address": "bc1qerj3ea5frs9zzqdwe65v6h8fhwl677a6s0hxhf", | ||||||
|  |                 "value": 294 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "0020c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a", | ||||||
|  |                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a", | ||||||
|  |                 "scriptpubkey_type": "v0_p2wsh", | ||||||
|  |                 "scriptpubkey_address": "bc1qcjzmhwqvf038dem74safsw3ernytrgd479sfjk3kc00lrq5k8pdqczl83q", | ||||||
|  |                 "value": 330 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "5120a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9", | ||||||
|  |                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9", | ||||||
|  |                 "scriptpubkey_type": "v1_p2tr", | ||||||
|  |                 "scriptpubkey_address": "bc1p57jzkf5f27sxe80y6unq780njt8y6mnmwsl44hp8g9ww9t7wkwusv7av76", | ||||||
|  |                 "value": 330 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "51024e73", | ||||||
|  |                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_2 4e73", | ||||||
|  |                 "scriptpubkey_type": "unknown", | ||||||
|  |                 "scriptpubkey_address": "bc1pfeessrawgf", | ||||||
|  |                 "value": 240 | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 "scriptpubkey": "6a224e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e005152535455565758595a5b5c5d5e5f60", | ||||||
|  |                 "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_34 4e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e OP_0 OP_PUSHNUM_1 OP_PUSHNUM_2 OP_PUSHNUM_3 OP_PUSHNUM_4 OP_PUSHNUM_5 OP_PUSHNUM_6 OP_PUSHNUM_7 OP_PUSHNUM_8 OP_PUSHNUM_9 OP_PUSHNUM_10 OP_PUSHNUM_11 OP_PUSHNUM_12 OP_PUSHNUM_13 OP_PUSHNUM_14 OP_PUSHNUM_15 OP_PUSHNUM_16", | ||||||
|  |                 "scriptpubkey_type": "op_return", | ||||||
|  |                 "value": 0 | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |         "size": 3500, | ||||||
|  |         "weight": 8186, | ||||||
|  |         "sigops": 115, | ||||||
|  |         "fee": 71294, | ||||||
|  |         "status": { | ||||||
|  |             "confirmed": true, | ||||||
|  |             "block_height": 850000, | ||||||
|  |             "block_hash": "00000000000000000002a0b5db2a7f8d9087464c2586b546be7bce8eb53b8187", | ||||||
|  |             "block_time": 1719689674 | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
| @ -20,9 +20,10 @@ describe('Mempool Backend Config', () => { | |||||||
|         BLOCKS_SUMMARIES_INDEXING: false, |         BLOCKS_SUMMARIES_INDEXING: false, | ||||||
|         GOGGLES_INDEXING: false, |         GOGGLES_INDEXING: false, | ||||||
|         HTTP_PORT: 8999, |         HTTP_PORT: 8999, | ||||||
|  |         UNIX_SOCKET_PATH: '', | ||||||
|         SPAWN_CLUSTER_PROCS: 0, |         SPAWN_CLUSTER_PROCS: 0, | ||||||
|         API_URL_PREFIX: '/api/v1/', |         API_URL_PREFIX: '/api/v1/', | ||||||
|         AUTOMATIC_BLOCK_REINDEXING: false, |         AUTOMATIC_POOLS_UPDATE: false, | ||||||
|         POLL_RATE_MS: 2000, |         POLL_RATE_MS: 2000, | ||||||
|         CACHE_DIR: './cache', |         CACHE_DIR: './cache', | ||||||
|         CACHE_ENABLED: true, |         CACHE_ENABLED: true, | ||||||
| @ -62,6 +63,7 @@ describe('Mempool Backend Config', () => { | |||||||
|         REQUEST_TIMEOUT: 10000, |         REQUEST_TIMEOUT: 10000, | ||||||
|         FALLBACK_TIMEOUT: 5000, |         FALLBACK_TIMEOUT: 5000, | ||||||
|         FALLBACK: [], |         FALLBACK: [], | ||||||
|  |         MAX_BEHIND_TIP: 2, | ||||||
|        }); |        }); | ||||||
| 
 | 
 | ||||||
|       expect(config.CORE_RPC).toStrictEqual({ |       expect(config.CORE_RPC).toStrictEqual({ | ||||||
| @ -134,6 +136,8 @@ describe('Mempool Backend Config', () => { | |||||||
|         ENABLED: false, |         ENABLED: false, | ||||||
|         AUDIT: false, |         AUDIT: false, | ||||||
|         AUDIT_START_HEIGHT: 774000, |         AUDIT_START_HEIGHT: 774000, | ||||||
|  |         STATISTICS: false, | ||||||
|  |         STATISTICS_START_TIME: 1481932800, | ||||||
|         SERVERS: [] |         SERVERS: [] | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
| @ -150,6 +154,7 @@ describe('Mempool Backend Config', () => { | |||||||
| 
 | 
 | ||||||
|       expect(config.FIAT_PRICE).toStrictEqual({ |       expect(config.FIAT_PRICE).toStrictEqual({ | ||||||
|         ENABLED: true, |         ENABLED: true, | ||||||
|  |         PAID: false, | ||||||
|         API_KEY: '', |         API_KEY: '', | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test- | |||||||
| 
 | 
 | ||||||
| describe('Rust GBT', () => { | describe('Rust GBT', () => { | ||||||
|   test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => { |   test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => { | ||||||
|     const rustGbt = new GbtGenerator(); |     const rustGbt = new GbtGenerator(4_000_000, 8); | ||||||
|     const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer); |     const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer); | ||||||
|     const result = await rustGbt.make(mempool, [], maxUid); |     const result = await rustGbt.make(mempool, [], maxUid); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -70,7 +70,7 @@ class AboutRoutes { | |||||||
|           res.status(500).end(); |           res.status(500).end(); | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => { |       .get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => { | ||||||
|         const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; |         const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; | ||||||
|         try { |         try { | ||||||
|           const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); |           const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); | ||||||
|  | |||||||
| @ -1,55 +1,50 @@ | |||||||
| import { Application, Request, Response } from "express"; | import { Application, Request, Response } from 'express'; | ||||||
| import config from "../../config"; | import config from '../../config'; | ||||||
| import axios from "axios"; | import axios from 'axios'; | ||||||
| import logger from "../../logger"; | import logger from '../../logger'; | ||||||
|  | import mempool from '../mempool'; | ||||||
|  | import AccelerationRepository from '../../repositories/AccelerationRepository'; | ||||||
| 
 | 
 | ||||||
| class AccelerationRoutes { | class AccelerationRoutes { | ||||||
|   private tag = 'Accelerator'; |   private tag = 'Accelerator'; | ||||||
| 
 | 
 | ||||||
|   public initRoutes(app: Application) { |   public initRoutes(app: Application): void { | ||||||
|     app |     app | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this)) |       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this)) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this)) |       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this)) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this)) |       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this)) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this)) |       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this)) | ||||||
|  |       .post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this)) | ||||||
|     ; |     ; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $getAcceleratorAccelerations(req: Request, res: Response) { |   private async $getAcceleratorAccelerations(req: Request, res: Response): Promise<void> { | ||||||
|     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; |     const accelerations = mempool.getAccelerations(); | ||||||
|     try { |     res.status(200).send(Object.values(accelerations)); | ||||||
|       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); |  | ||||||
|       for (const key in response.headers) { |  | ||||||
|         res.setHeader(key, response.headers[key]);  |  | ||||||
|       }       |  | ||||||
|       response.data.pipe(res); |  | ||||||
|     } catch (e) { |  | ||||||
|       logger.err(`Unable to get current accelerations from ${url} in $getAcceleratorAccelerations(), ${e}`, this.tag); |  | ||||||
|       res.status(500).end(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $getAcceleratorAccelerationsHistory(req: Request, res: Response) { |   private async $getAcceleratorAccelerationsHistory(req: Request, res: Response): Promise<void> { | ||||||
|     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; |     const history = await AccelerationRepository.$getAccelerationInfo(null, req.query.blockHeight ? parseInt(req.query.blockHeight as string, 10) : null); | ||||||
|     try { |     res.status(200).send(history.map(accel => ({ | ||||||
|       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); |       txid: accel.txid, | ||||||
|       for (const key in response.headers) { |       added: accel.added, | ||||||
|         res.setHeader(key, response.headers[key]);  |       status: 'completed', | ||||||
|       }       |       effectiveFee: accel.effective_fee, | ||||||
|       response.data.pipe(res); |       effectiveVsize: accel.effective_vsize, | ||||||
|     } catch (e) { |       boostRate: accel.boost_rate, | ||||||
|       logger.err(`Unable to get acceleration history from ${url} in $getAcceleratorAccelerationsHistory(), ${e}`, this.tag); |       boostCost: accel.boost_cost, | ||||||
|       res.status(500).end(); |       blockHeight: accel.height, | ||||||
|     } |       pools: [accel.pool], | ||||||
|  |     }))); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response) { |   private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response): Promise<void> { | ||||||
|     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; |     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; | ||||||
|     try { |     try { | ||||||
|       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); |       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); | ||||||
|       for (const key in response.headers) { |       for (const key in response.headers) { | ||||||
|         res.setHeader(key, response.headers[key]);  |         res.setHeader(key, response.headers[key]); | ||||||
|       }       |       } | ||||||
|       response.data.pipe(res); |       response.data.pipe(res); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag); |       logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag); | ||||||
| @ -57,19 +52,33 @@ class AccelerationRoutes { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async $getAcceleratorAccelerationsStats(req: Request, res: Response) { |   private async $getAcceleratorAccelerationsStats(req: Request, res: Response): Promise<void> { | ||||||
|     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; |     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; | ||||||
|     try { |     try { | ||||||
|       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); |       const response = await axios.get(url, { responseType: 'stream', timeout: 10000 }); | ||||||
|       for (const key in response.headers) { |       for (const key in response.headers) { | ||||||
|         res.setHeader(key, response.headers[key]);  |         res.setHeader(key, response.headers[key]); | ||||||
|       }       |       } | ||||||
|       response.data.pipe(res); |       response.data.pipe(res); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag); |       logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag); | ||||||
|       res.status(500).end(); |       res.status(500).end(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> { | ||||||
|  |     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; | ||||||
|  |     try { | ||||||
|  |       const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 }); | ||||||
|  |       for (const key in response.headers) { | ||||||
|  |         res.setHeader(key, response.headers[key]); | ||||||
|  |       } | ||||||
|  |       response.data.pipe(res); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag); | ||||||
|  |       res.status(500).end(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new AccelerationRoutes(); | export default new AccelerationRoutes(); | ||||||
| @ -1,15 +1,14 @@ | |||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| import { MempoolTransactionExtended } from '../../mempool.interfaces'; | import { MempoolTransactionExtended } from '../../mempool.interfaces'; | ||||||
| import { IEsploraApi } from '../bitcoin/esplora-api.interface'; | import { GraphTx, getSameBlockRelatives, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from '../mini-miner'; | ||||||
| 
 | 
 | ||||||
| const BLOCK_WEIGHT_UNITS = 4_000_000; | const BLOCK_WEIGHT_UNITS = 4_000_000; | ||||||
| const BLOCK_SIGOPS = 80_000; |  | ||||||
| const MAX_RELATIVE_GRAPH_SIZE = 200; | const MAX_RELATIVE_GRAPH_SIZE = 200; | ||||||
| const BID_BOOST_WINDOW = 40_000; | const BID_BOOST_WINDOW = 40_000; | ||||||
| const BID_BOOST_MIN_OFFSET = 10_000; | const BID_BOOST_MIN_OFFSET = 10_000; | ||||||
| const BID_BOOST_MAX_OFFSET = 400_000; | const BID_BOOST_MAX_OFFSET = 400_000; | ||||||
| 
 | 
 | ||||||
| type Acceleration = { | export type Acceleration = { | ||||||
|   txid: string; |   txid: string; | ||||||
|   max_bid: number; |   max_bid: number; | ||||||
| }; | }; | ||||||
| @ -28,31 +27,6 @@ export interface AccelerationInfo { | |||||||
|   cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
 |   cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface GraphTx { |  | ||||||
|   txid: string; |  | ||||||
|   vsize: number; |  | ||||||
|   weight: number; |  | ||||||
|   fees: { |  | ||||||
|     base: number; // in sats
 |  | ||||||
|   }; |  | ||||||
|   depends: string[]; |  | ||||||
|   spentby: string[]; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface MempoolTx extends GraphTx { |  | ||||||
|   ancestorcount: number; |  | ||||||
|   ancestorsize: number; |  | ||||||
|   fees: { // in sats
 |  | ||||||
|     base: number; |  | ||||||
|     ancestor: number; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   ancestors: Map<string, MempoolTx>, |  | ||||||
|   ancestorRate: number; |  | ||||||
|   individualRate: number; |  | ||||||
|   score: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class AccelerationCosts { | class AccelerationCosts { | ||||||
|   /** |   /** | ||||||
|    * Takes a list of accelerations and verbose block data |    * Takes a list of accelerations and verbose block data | ||||||
| @ -61,7 +35,7 @@ class AccelerationCosts { | |||||||
|    * @param accelerationsx |    * @param accelerationsx | ||||||
|    * @param verboseBlock |    * @param verboseBlock | ||||||
|    */ |    */ | ||||||
|   public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number { |   public calculateBoostRate(accelerations: Acceleration[], blockTxs: MempoolTransactionExtended[]): number { | ||||||
|     // Run GBT ourselves to calculate accurate effective fee rates
 |     // Run GBT ourselves to calculate accurate effective fee rates
 | ||||||
|     // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
 |     // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
 | ||||||
|     const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity); |     const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity); | ||||||
| @ -170,108 +144,28 @@ class AccelerationCosts { | |||||||
|   /** |   /** | ||||||
|    * Takes an accelerated mined txid and a target rate |    * Takes an accelerated mined txid and a target rate | ||||||
|    * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors |    * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors | ||||||
|    *  |    * | ||||||
|    * @param txid  |    * @param txid | ||||||
|    * @param medianFeeRate  |    * @param medianFeeRate | ||||||
|    */ |    */ | ||||||
|   public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { |   public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo { | ||||||
|     // Get same-block transaction ancestors
 |     // Get same-block transaction ancestors
 | ||||||
|     const allRelatives = this.getSameBlockRelatives(tx, transactions); |     const allRelatives = getSameBlockRelatives(tx, transactions); | ||||||
|     const relativesMap = this.initializeRelatives(allRelatives); |     const relativesMap = initializeRelatives(allRelatives); | ||||||
|     const rootTx = relativesMap.get(tx.txid) as MempoolTx; |     const rootTx = relativesMap.get(tx.txid) as GraphTx; | ||||||
| 
 | 
 | ||||||
|     // Calculate cost to boost
 |     // Calculate cost to boost
 | ||||||
|     return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate); |     return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |  | ||||||
|    * Takes a raw transaction, and builds a graph of same-block relatives, |  | ||||||
|    * and returns as a MempoolTx |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> { |  | ||||||
|     const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
 |  | ||||||
|     const spendMap = new Map<string, string>(); // map of outpoints to spending txids
 |  | ||||||
|     for (const tx of transactions) { |  | ||||||
|       blockTxs.set(tx.txid, tx); |  | ||||||
|       for (const vin of tx.vin) { |  | ||||||
|         spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const relatives: Map<string, GraphTx> = new Map(); |  | ||||||
|     const stack: string[] = [tx.txid]; |  | ||||||
| 
 |  | ||||||
|     // build set of same-block ancestors
 |  | ||||||
|     while (stack.length > 0) { |  | ||||||
|       const nextTxid = stack.pop(); |  | ||||||
|       const nextTx = nextTxid ? blockTxs.get(nextTxid) : null; |  | ||||||
|       if (!nextTx || relatives.has(nextTx.txid)) { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const mempoolTx = this.convertToGraphTx(nextTx); |  | ||||||
| 
 |  | ||||||
|       mempoolTx.fees.base = nextTx.fee || 0; |  | ||||||
|       mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[]; |  | ||||||
|       mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[]; |  | ||||||
| 
 |  | ||||||
|       for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) { |  | ||||||
|         if (txid) { |  | ||||||
|           stack.push(txid); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       relatives.set(mempoolTx.txid, mempoolTx); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return relatives; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Takes a raw transaction and converts it to MempoolTx format |  | ||||||
|    * fee and ancestor data is initialized with dummy/null values |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx { |  | ||||||
|     return { |  | ||||||
|       txid: tx.txid, |  | ||||||
|       vsize: Math.ceil(tx.weight / 4), |  | ||||||
|       weight: tx.weight, |  | ||||||
|       fees: { |  | ||||||
|         base: 0, // dummy
 |  | ||||||
|       }, |  | ||||||
|       depends: [], // dummy
 |  | ||||||
|       spentby: [], //dummy
 |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private convertGraphToMempoolTx(tx: GraphTx): MempoolTx { |  | ||||||
|     return { |  | ||||||
|       ...tx, |  | ||||||
|       fees: { |  | ||||||
|         base: tx.fees.base, |  | ||||||
|         ancestor: tx.fees.base, |  | ||||||
|       }, |  | ||||||
|       ancestorcount: 1, |  | ||||||
|       ancestorsize: Math.ceil(tx.weight / 4), |  | ||||||
|       ancestors: new Map<string, MempoolTx>(), |  | ||||||
|       ancestorRate: 0, |  | ||||||
|       individualRate: 0, |  | ||||||
|       score: 0, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |   /** | ||||||
|    * Given a root transaction, a list of in-mempool ancestors, and a target fee rate, |    * Given a root transaction, a list of in-mempool ancestors, and a target fee rate, | ||||||
|    * Calculate the minimum set of transactions to fee-bump, their total vsize + fees |    * Calculate the minimum set of transactions to fee-bump, their total vsize + fees | ||||||
|    *  |    * | ||||||
|    * @param tx |    * @param tx | ||||||
|    * @param ancestors |    * @param ancestors | ||||||
|    */ |    */ | ||||||
|   private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo { |   private calculateAccelerationAncestors(tx: GraphTx, relatives: Map<string, GraphTx>, targetFeeRate: number): AccelerationInfo { | ||||||
|     // add root tx to the ancestor map
 |     // add root tx to the ancestor map
 | ||||||
|     relatives.set(tx.txid, tx); |     relatives.set(tx.txid, tx); | ||||||
| 
 | 
 | ||||||
| @ -283,12 +177,12 @@ class AccelerationCosts { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Initialize individual & ancestor fee rates
 |     // Initialize individual & ancestor fee rates
 | ||||||
|     relatives.forEach(entry => this.setAncestorScores(entry)); |     relatives.forEach(entry => setAncestorScores(entry)); | ||||||
| 
 | 
 | ||||||
|     // Sort by descending ancestor score
 |     // Sort by descending ancestor score
 | ||||||
|     let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); |     let sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator); | ||||||
| 
 | 
 | ||||||
|     let includedInCluster: Map<string, MempoolTx> | null = null; |     let includedInCluster: Map<string, GraphTx> | null = null; | ||||||
| 
 | 
 | ||||||
|     // While highest score >= targetFeeRate
 |     // While highest score >= targetFeeRate
 | ||||||
|     let maxIterations = MAX_RELATIVE_GRAPH_SIZE; |     let maxIterations = MAX_RELATIVE_GRAPH_SIZE; | ||||||
| @ -297,17 +191,17 @@ class AccelerationCosts { | |||||||
|       // Grab the highest scoring entry
 |       // Grab the highest scoring entry
 | ||||||
|       const best = sortedRelatives.shift(); |       const best = sortedRelatives.shift(); | ||||||
|       if (best) { |       if (best) { | ||||||
|         const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []); |         const cluster = new Map<string, GraphTx>(best.ancestors?.entries() || []); | ||||||
|         if (best.ancestors.has(tx.txid)) { |         if (best.ancestors.has(tx.txid)) { | ||||||
|           includedInCluster = cluster; |           includedInCluster = cluster; | ||||||
|         } |         } | ||||||
|         cluster.set(best.txid, best); |         cluster.set(best.txid, best); | ||||||
|         // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
 |         // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
 | ||||||
|         // and update scores, ancestor totals and dependencies for the survivors
 |         // and update scores, ancestor totals and dependencies for the survivors
 | ||||||
|         this.removeAncestors(cluster, relatives); |         removeAncestors(cluster, relatives); | ||||||
| 
 | 
 | ||||||
|         // re-sort
 |         // re-sort
 | ||||||
|         sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator); |         sortedRelatives = Array.from(relatives.values()).sort(mempoolComparator); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -345,394 +239,6 @@ class AccelerationCosts { | |||||||
|       nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate), |       nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors |  | ||||||
|    * for each transaction. |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|   private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> { |  | ||||||
|     // sanity check for infinite recursion / too many ancestors (should never happen)
 |  | ||||||
|     if (depth >= 100) { |  | ||||||
|       logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`); |  | ||||||
|       throw new Error('invalid_tx_dependencies'); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // initialize the ancestor map for this tx
 |  | ||||||
|     tx.ancestors = new Map<string, MempoolTx>(); |  | ||||||
|     tx.depends.forEach(parentId => { |  | ||||||
|       const parent = all.get(parentId); |  | ||||||
|       if (parent) { |  | ||||||
|         // add the parent
 |  | ||||||
|         tx.ancestors?.set(parentId, parent); |  | ||||||
|         // check for a cached copy of this parent's ancestors
 |  | ||||||
|         let ancestors = visited.get(parent.txid); |  | ||||||
|         if (!ancestors) { |  | ||||||
|           // recursively fetch the parent's ancestors
 |  | ||||||
|           ancestors = this.setAncestors(parent, all, visited, depth + 1); |  | ||||||
|         } |  | ||||||
|         // and add to this tx's map
 |  | ||||||
|         ancestors.forEach((ancestor, ancestorId) => { |  | ||||||
|           tx.ancestors?.set(ancestorId, ancestor); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     visited.set(tx.txid, tx.ancestors); |  | ||||||
| 
 |  | ||||||
|     return tx.ancestors; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph |  | ||||||
|    * by running setAncestors on each leaf, and caching intermediate results. |  | ||||||
|    * then initializes ancestor data for each transaction |  | ||||||
|    *  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|   private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> { |  | ||||||
|     const mempoolTxs = new Map<string, MempoolTx>(); |  | ||||||
|     all.forEach(entry => { |  | ||||||
|       mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry)); |  | ||||||
|     }); |  | ||||||
|     const visited: Map<string, Map<string, MempoolTx>> = new Map(); |  | ||||||
|     const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); |  | ||||||
|     for (const leaf of leaves) { |  | ||||||
|       this.setAncestors(leaf, mempoolTxs, visited); |  | ||||||
|     } |  | ||||||
|     mempoolTxs.forEach(entry => { |  | ||||||
|       entry.ancestors?.forEach(ancestor => { |  | ||||||
|         entry.ancestorcount++; |  | ||||||
|         entry.ancestorsize += ancestor.vsize; |  | ||||||
|         entry.fees.ancestor += ancestor.fees.base; |  | ||||||
|       }); |  | ||||||
|       this.setAncestorScores(entry); |  | ||||||
|     }); |  | ||||||
|     return mempoolTxs; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Remove a cluster of transactions from an in-mempool dependency graph |  | ||||||
|    * and update the survivors' scores and ancestors |  | ||||||
|    *  |  | ||||||
|    * @param cluster  |  | ||||||
|    * @param ancestors  |  | ||||||
|    */ |  | ||||||
|   private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void { |  | ||||||
|     // remove
 |  | ||||||
|     cluster.forEach(tx => { |  | ||||||
|       all.delete(tx.txid); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // update survivors
 |  | ||||||
|     all.forEach(tx => { |  | ||||||
|       cluster.forEach(remove => { |  | ||||||
|         if (tx.ancestors?.has(remove.txid)) { |  | ||||||
|           // remove as dependency
 |  | ||||||
|           tx.ancestors.delete(remove.txid); |  | ||||||
|           tx.depends = tx.depends.filter(parent => parent !== remove.txid); |  | ||||||
|           // update ancestor sizes and fees
 |  | ||||||
|           tx.ancestorsize -= remove.vsize; |  | ||||||
|           tx.fees.ancestor -= remove.fees.base; |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       // recalculate fee rates
 |  | ||||||
|       this.setAncestorScores(tx); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Take a mempool transaction, and set the fee rates and ancestor score |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
|   private setAncestorScores(tx: MempoolTx): void { |  | ||||||
|     tx.individualRate = tx.fees.base / tx.vsize; |  | ||||||
|     tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize; |  | ||||||
|     tx.score = Math.min(tx.individualRate, tx.ancestorRate); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Sort by descending score
 |  | ||||||
|   private mempoolComparator(a, b): number { |  | ||||||
|     return b.score - a.score; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new AccelerationCosts; | export default new AccelerationCosts; | ||||||
| 
 |  | ||||||
| interface TemplateTransaction { |  | ||||||
|   txid: string; |  | ||||||
|   order: number; |  | ||||||
|   weight: number; |  | ||||||
|   adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
 |  | ||||||
|   sigops: number; |  | ||||||
|   fee: number; |  | ||||||
|   feeDelta: number; |  | ||||||
|   ancestors: string[]; |  | ||||||
|   cluster: string[]; |  | ||||||
|   effectiveFeePerVsize: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface MinerTransaction extends TemplateTransaction { |  | ||||||
|   inputs: string[]; |  | ||||||
|   feePerVsize: number; |  | ||||||
|   relativesSet: boolean; |  | ||||||
|   ancestorMap: Map<string, MinerTransaction>; |  | ||||||
|   children: Set<MinerTransaction>; |  | ||||||
|   ancestorFee: number; |  | ||||||
|   ancestorVsize: number; |  | ||||||
|   ancestorSigops: number; |  | ||||||
|   score: number; |  | ||||||
|   used: boolean; |  | ||||||
|   modified: boolean; |  | ||||||
|   dependencyRate: number; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
| * Build a block using an approximation of the transaction selection algorithm from Bitcoin Core |  | ||||||
| * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 |  | ||||||
| */ |  | ||||||
| export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] { |  | ||||||
|   const auditPool: Map<string, MinerTransaction> = new Map(); |  | ||||||
|   const mempoolArray: MinerTransaction[] = []; |  | ||||||
|    |  | ||||||
|   candidates.forEach(tx => { |  | ||||||
|     // initializing everything up front helps V8 optimize property access later
 |  | ||||||
|     const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0))); |  | ||||||
|     const feePerVsize = (tx.fee / adjustedVsize); |  | ||||||
|     auditPool.set(tx.txid, { |  | ||||||
|       txid: tx.txid, |  | ||||||
|       order: txidToOrdering(tx.txid), |  | ||||||
|       fee: tx.fee, |  | ||||||
|       feeDelta: 0, |  | ||||||
|       weight: tx.weight, |  | ||||||
|       adjustedVsize, |  | ||||||
|       feePerVsize: feePerVsize, |  | ||||||
|       effectiveFeePerVsize: feePerVsize, |  | ||||||
|       dependencyRate: feePerVsize, |  | ||||||
|       sigops: tx.sigops || 0, |  | ||||||
|       inputs: (tx.vin?.map(vin => vin.txid) || []) as string[], |  | ||||||
|       relativesSet: false, |  | ||||||
|       ancestors: [], |  | ||||||
|       cluster: [], |  | ||||||
|       ancestorMap: new Map<string, MinerTransaction>(), |  | ||||||
|       children: new Set<MinerTransaction>(), |  | ||||||
|       ancestorFee: 0, |  | ||||||
|       ancestorVsize: 0, |  | ||||||
|       ancestorSigops: 0, |  | ||||||
|       score: 0, |  | ||||||
|       used: false, |  | ||||||
|       modified: false, |  | ||||||
|     }); |  | ||||||
|     mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // set accelerated effective fee
 |  | ||||||
|   for (const acceleration of accelerations) { |  | ||||||
|     const tx = auditPool.get(acceleration.txid); |  | ||||||
|     if (tx) { |  | ||||||
|       tx.feeDelta = acceleration.max_bid; |  | ||||||
|       tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize); |  | ||||||
|       tx.effectiveFeePerVsize = tx.feePerVsize; |  | ||||||
|       tx.dependencyRate = tx.feePerVsize; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Build relatives graph & calculate ancestor scores
 |  | ||||||
|   for (const tx of mempoolArray) { |  | ||||||
|     if (!tx.relativesSet) { |  | ||||||
|       setRelatives(tx, auditPool); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Sort by descending ancestor score
 |  | ||||||
|   mempoolArray.sort(priorityComparator); |  | ||||||
| 
 |  | ||||||
|   // Build blocks by greedily choosing the highest feerate package
 |  | ||||||
|   // (i.e. the package rooted in the transaction with the best ancestor score)
 |  | ||||||
|   const blocks: number[][] = []; |  | ||||||
|   let blockWeight = 0; |  | ||||||
|   let blockSigops = 0; |  | ||||||
|   const transactions: MinerTransaction[] = []; |  | ||||||
|   let modified: MinerTransaction[] = []; |  | ||||||
|   const overflow: MinerTransaction[] = []; |  | ||||||
|   let failures = 0; |  | ||||||
|   while (mempoolArray.length || modified.length) { |  | ||||||
|     // skip invalid transactions
 |  | ||||||
|     while (mempoolArray[0].used || mempoolArray[0].modified) { |  | ||||||
|       mempoolArray.shift(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Select best next package
 |  | ||||||
|     let nextTx; |  | ||||||
|     const nextPoolTx = mempoolArray[0]; |  | ||||||
|     const nextModifiedTx = modified[0]; |  | ||||||
|     if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) { |  | ||||||
|       nextTx = nextPoolTx; |  | ||||||
|       mempoolArray.shift(); |  | ||||||
|     } else { |  | ||||||
|       modified.shift(); |  | ||||||
|       if (nextModifiedTx) { |  | ||||||
|         nextTx = nextModifiedTx; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (nextTx && !nextTx?.used) { |  | ||||||
|       // Check if the package fits into this block
 |  | ||||||
|       if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) { |  | ||||||
|         const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values()); |  | ||||||
|         // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 |  | ||||||
|         const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; |  | ||||||
|         const clusterTxids = sortedTxSet.map(tx => tx.txid); |  | ||||||
|         const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize); |  | ||||||
|         const used: MinerTransaction[] = []; |  | ||||||
|         while (sortedTxSet.length) { |  | ||||||
|           const ancestor = sortedTxSet.pop(); |  | ||||||
|           if (!ancestor) { |  | ||||||
|             continue; |  | ||||||
|           } |  | ||||||
|           ancestor.used = true; |  | ||||||
|           ancestor.usedBy = nextTx.txid; |  | ||||||
|           // update this tx with effective fee rate & relatives data
 |  | ||||||
|           if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) { |  | ||||||
|             ancestor.effectiveFeePerVsize = effectiveFeeRate; |  | ||||||
|           } |  | ||||||
|           ancestor.cluster = clusterTxids; |  | ||||||
|           transactions.push(ancestor); |  | ||||||
|           blockWeight += ancestor.weight; |  | ||||||
|           blockSigops += ancestor.sigops; |  | ||||||
|           used.push(ancestor); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // remove these as valid package ancestors for any descendants remaining in the mempool
 |  | ||||||
|         if (used.length) { |  | ||||||
|           used.forEach(tx => { |  | ||||||
|             modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         failures = 0; |  | ||||||
|       } else { |  | ||||||
|         // hold this package in an overflow list while we check for smaller options
 |  | ||||||
|         overflow.push(nextTx); |  | ||||||
|         failures++; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // this block is full
 |  | ||||||
|     const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000); |  | ||||||
|     const queueEmpty = !mempoolArray.length && !modified.length; |  | ||||||
| 
 |  | ||||||
|     if (exceededPackageTries || queueEmpty) { |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   for (const tx of transactions) { |  | ||||||
|     tx.ancestors = Object.values(tx.ancestorMap); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return transactions; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // traverse in-mempool ancestors
 |  | ||||||
| // recursion unavoidable, but should be limited to depth < 25 by mempool policy
 |  | ||||||
| function setRelatives( |  | ||||||
|   tx: MinerTransaction, |  | ||||||
|   mempool: Map<string, MinerTransaction>, |  | ||||||
| ): void { |  | ||||||
|   for (const parent of tx.inputs) { |  | ||||||
|     const parentTx = mempool.get(parent); |  | ||||||
|     if (parentTx && !tx.ancestorMap?.has(parent)) { |  | ||||||
|       tx.ancestorMap.set(parent, parentTx); |  | ||||||
|       parentTx.children.add(tx); |  | ||||||
|       // visit each node only once
 |  | ||||||
|       if (!parentTx.relativesSet) { |  | ||||||
|         setRelatives(parentTx, mempool); |  | ||||||
|       } |  | ||||||
|       parentTx.ancestorMap.forEach((ancestor) => { |  | ||||||
|         tx.ancestorMap.set(ancestor.txid, ancestor); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|   tx.ancestorFee = (tx.fee + tx.feeDelta); |  | ||||||
|   tx.ancestorVsize = tx.adjustedVsize || 0; |  | ||||||
|   tx.ancestorSigops = tx.sigops || 0; |  | ||||||
|   tx.ancestorMap.forEach((ancestor) => { |  | ||||||
|     tx.ancestorFee += (ancestor.fee + ancestor.feeDelta); |  | ||||||
|     tx.ancestorVsize += ancestor.adjustedVsize; |  | ||||||
|     tx.ancestorSigops += ancestor.sigops; |  | ||||||
|   }); |  | ||||||
|   tx.score = tx.ancestorFee / tx.ancestorVsize; |  | ||||||
|   tx.relativesSet = true; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 |  | ||||||
| // avoids recursion to limit call stack depth
 |  | ||||||
| function updateDescendants( |  | ||||||
|   rootTx: MinerTransaction, |  | ||||||
|   mempool: Map<string, MinerTransaction>, |  | ||||||
|   modified: MinerTransaction[], |  | ||||||
|   clusterRate: number, |  | ||||||
| ): MinerTransaction[] { |  | ||||||
|   const descendantSet: Set<MinerTransaction> = new Set(); |  | ||||||
|   // stack of nodes left to visit
 |  | ||||||
|   const descendants: MinerTransaction[] = []; |  | ||||||
|   let descendantTx: MinerTransaction | undefined; |  | ||||||
|   rootTx.children.forEach(childTx => { |  | ||||||
|     if (!descendantSet.has(childTx)) { |  | ||||||
|       descendants.push(childTx); |  | ||||||
|       descendantSet.add(childTx); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   while (descendants.length) { |  | ||||||
|     descendantTx = descendants.pop(); |  | ||||||
|     if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { |  | ||||||
|       // remove tx as ancestor
 |  | ||||||
|       descendantTx.ancestorMap.delete(rootTx.txid); |  | ||||||
|       descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta); |  | ||||||
|       descendantTx.ancestorVsize -= rootTx.adjustedVsize; |  | ||||||
|       descendantTx.ancestorSigops -= rootTx.sigops; |  | ||||||
|       descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize; |  | ||||||
|       descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate; |  | ||||||
| 
 |  | ||||||
|       if (!descendantTx.modified) { |  | ||||||
|         descendantTx.modified = true; |  | ||||||
|         modified.push(descendantTx); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // add this node's children to the stack
 |  | ||||||
|       descendantTx.children.forEach(childTx => { |  | ||||||
|         // visit each node only once
 |  | ||||||
|         if (!descendantSet.has(childTx)) { |  | ||||||
|           descendants.push(childTx); |  | ||||||
|           descendantSet.add(childTx); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   // return new, resorted modified list
 |  | ||||||
|   return modified.sort(priorityComparator); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Used to sort an array of MinerTransactions by descending ancestor score
 |  | ||||||
| function priorityComparator(a: MinerTransaction, b: MinerTransaction): number { |  | ||||||
|   if (b.score === a.score) { |  | ||||||
|     // tie-break by txid for stability
 |  | ||||||
|     return a.order - b.order; |  | ||||||
|   } else { |  | ||||||
|     return b.score - a.score; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // returns the most significant 4 bytes of the txid as an integer
 |  | ||||||
| function txidToOrdering(txid: string): number { |  | ||||||
|   return parseInt( |  | ||||||
|     txid.substring(62, 64) + |  | ||||||
|       txid.substring(60, 62) + |  | ||||||
|       txid.substring(58, 60) + |  | ||||||
|       txid.substring(56, 58), |  | ||||||
|     16 |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @ -2,24 +2,28 @@ import config from '../config'; | |||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; | import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; | ||||||
| import rbfCache from './rbf-cache'; | import rbfCache from './rbf-cache'; | ||||||
|  | import transactionUtils from './transaction-utils'; | ||||||
| 
 | 
 | ||||||
| const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
 | ||||||
| 
 | 
 | ||||||
| class Audit { | class Audit { | ||||||
|   auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false) |   auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }) | ||||||
|    : { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { |    : { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } { | ||||||
|     if (!projectedBlocks?.[0]?.transactionIds || !mempool) { |     if (!projectedBlocks?.[0]?.transactionIds || !mempool) { | ||||||
|       return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; |       return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const matches: string[] = []; // present in both mined block and template
 |     const matches: string[] = []; // present in both mined block and template
 | ||||||
|     const added: string[] = []; // present in mined block, not in template
 |     const added: string[] = []; // present in mined block, not in template
 | ||||||
|     const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
 |     const unseen: string[] = []; // present in the mined block, not in our mempool
 | ||||||
|  |     let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
 | ||||||
|  |     let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
 | ||||||
|     const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
 |     const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
 | ||||||
|     const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
 |     const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
 | ||||||
|     const accelerated: string[] = []; // prioritized by the mempool accelerator
 |     const accelerated: string[] = []; // prioritized by the mempool accelerator
 | ||||||
|     const isCensored = {}; // missing, without excuse
 |     const isCensored = {}; // missing, without excuse
 | ||||||
|     const isDisplaced = {}; |     const isDisplaced = {}; | ||||||
|  |     const isAccelerated = {}; | ||||||
|     let displacedWeight = 0; |     let displacedWeight = 0; | ||||||
|     let matchedWeight = 0; |     let matchedWeight = 0; | ||||||
|     let projectedWeight = 0; |     let projectedWeight = 0; | ||||||
| @ -32,6 +36,7 @@ class Audit { | |||||||
|       inBlock[tx.txid] = tx; |       inBlock[tx.txid] = tx; | ||||||
|       if (mempool[tx.txid] && mempool[tx.txid].acceleration) { |       if (mempool[tx.txid] && mempool[tx.txid].acceleration) { | ||||||
|         accelerated.push(tx.txid); |         accelerated.push(tx.txid); | ||||||
|  |         isAccelerated[tx.txid] = true; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // coinbase is always expected
 |     // coinbase is always expected
 | ||||||
| @ -75,10 +80,6 @@ class Audit { | |||||||
|     let failures = 0; |     let failures = 0; | ||||||
|     let blockIndex = 1; |     let blockIndex = 1; | ||||||
|     while (projectedBlocks[blockIndex] && failures < 500) { |     while (projectedBlocks[blockIndex] && failures < 500) { | ||||||
|       if (index >= projectedBlocks[blockIndex].transactionIds.length) { |  | ||||||
|         index = 0; |  | ||||||
|         blockIndex++; |  | ||||||
|       } |  | ||||||
|       const txid = projectedBlocks[blockIndex].transactionIds[index]; |       const txid = projectedBlocks[blockIndex].transactionIds[index]; | ||||||
|       const tx = mempool[txid]; |       const tx = mempool[txid]; | ||||||
|       if (tx) { |       if (tx) { | ||||||
| @ -102,6 +103,10 @@ class Audit { | |||||||
|         logger.warn('projected transaction missing from mempool cache'); |         logger.warn('projected transaction missing from mempool cache'); | ||||||
|       } |       } | ||||||
|       index++; |       index++; | ||||||
|  |       if (index >= projectedBlocks[blockIndex].transactionIds.length) { | ||||||
|  |         index = 0; | ||||||
|  |         blockIndex++; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // mark unexpected transactions in the mined block as 'added'
 |     // mark unexpected transactions in the mined block as 'added'
 | ||||||
| @ -113,11 +118,16 @@ class Audit { | |||||||
|       } else { |       } else { | ||||||
|         if (rbfCache.has(tx.txid)) { |         if (rbfCache.has(tx.txid)) { | ||||||
|           rbf.push(tx.txid); |           rbf.push(tx.txid); | ||||||
|         } else if (!isDisplaced[tx.txid]) { |           if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) { | ||||||
|  |             unseen.push(tx.txid); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|           if (mempool[tx.txid]) { |           if (mempool[tx.txid]) { | ||||||
|             prioritized.push(tx.txid); |             if (isDisplaced[tx.txid]) { | ||||||
|  |               added.push(tx.txid); | ||||||
|  |             } | ||||||
|           } else { |           } else { | ||||||
|             added.push(tx.txid); |             unseen.push(tx.txid); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         overflowWeight += tx.weight; |         overflowWeight += tx.weight; | ||||||
| @ -125,6 +135,8 @@ class Audit { | |||||||
|       totalWeight += tx.weight; |       totalWeight += tx.weight; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     ({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize')); | ||||||
|  | 
 | ||||||
|     // transactions missing from near the end of our template are probably not being censored
 |     // transactions missing from near the end of our template are probably not being censored
 | ||||||
|     let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); |     let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); | ||||||
|     let maxOverflowRate = 0; |     let maxOverflowRate = 0; | ||||||
| @ -165,6 +177,7 @@ class Audit { | |||||||
|     const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; |     const similarity = projectedWeight ? matchedWeight / projectedWeight : 1; | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  |       unseen, | ||||||
|       censored: Object.keys(isCensored), |       censored: Object.keys(isCensored), | ||||||
|       added, |       added, | ||||||
|       prioritized, |       prioritized, | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { IBitcoinApi } from './bitcoin-api.interface'; | import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||||
| import { IEsploraApi } from './esplora-api.interface'; | import { IEsploraApi } from './esplora-api.interface'; | ||||||
| 
 | 
 | ||||||
| export interface AbstractBitcoinApi { | export interface AbstractBitcoinApi { | ||||||
| @ -22,11 +22,13 @@ export interface AbstractBitcoinApi { | |||||||
|   $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>; |   $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>; | ||||||
|   $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; |   $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; | ||||||
|   $sendRawTransaction(rawTransaction: string): Promise<string>; |   $sendRawTransaction(rawTransaction: string): Promise<string>; | ||||||
|  |   $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; | ||||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; |   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; | ||||||
|   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; |   $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; | ||||||
|   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; |   $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; | ||||||
|   $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>; |   $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>; | ||||||
|   $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>; |   $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>; | ||||||
|  |   $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>; | ||||||
| 
 | 
 | ||||||
|   startHealthChecks(): void; |   startHealthChecks(): void; | ||||||
|   getHealthStatus(): HealthCheckHost[]; |   getHealthStatus(): HealthCheckHost[]; | ||||||
|  | |||||||
| @ -205,3 +205,16 @@ export namespace IBitcoinApi { | |||||||
|     "utxo_size_inc": number; |     "utxo_size_inc": number; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface TestMempoolAcceptResult { | ||||||
|  |   txid: string, | ||||||
|  |   wtxid: string, | ||||||
|  |   allowed?: boolean, | ||||||
|  |   vsize?: number, | ||||||
|  |   fees?: { | ||||||
|  |     base: number, | ||||||
|  |     "effective-feerate": number, | ||||||
|  |     "effective-includes": string[], | ||||||
|  |   }, | ||||||
|  |   ['reject-reason']?: string, | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import * as bitcoinjs from 'bitcoinjs-lib'; | import * as bitcoinjs from 'bitcoinjs-lib'; | ||||||
| import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; | import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; | ||||||
| import { IBitcoinApi } from './bitcoin-api.interface'; | import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||||
| import { IEsploraApi } from './esplora-api.interface'; | import { IEsploraApi } from './esplora-api.interface'; | ||||||
| import blocks from '../blocks'; | import blocks from '../blocks'; | ||||||
| import mempool from '../mempool'; | import mempool from '../mempool'; | ||||||
| @ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|       .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); |       .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> { |   async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> { | ||||||
|     throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.'); |     const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2); | ||||||
|  |     const transactions: IEsploraApi.Transaction[] = []; | ||||||
|  |     for (const tx of verboseBlock.tx) { | ||||||
|  |       const converted = await this.$convertTransaction(tx, true); | ||||||
|  |       transactions.push(converted); | ||||||
|  |     } | ||||||
|  |     return transactions; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   $getRawBlock(hash: string): Promise<Buffer> { |   $getRawBlock(hash: string): Promise<Buffer> { | ||||||
| @ -159,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|     const mp = mempool.getMempool(); |     const mp = mempool.getMempool(); | ||||||
|     for (const tx in mp) { |     for (const tx in mp) { | ||||||
|       for (const vout of mp[tx].vout) { |       for (const vout of mp[tx].vout) { | ||||||
|         if (vout.scriptpubkey_address.indexOf(prefix) === 0) { |         if (vout.scriptpubkey_address?.indexOf(prefix) === 0) { | ||||||
|           found[vout.scriptpubkey_address] = ''; |           found[vout.scriptpubkey_address] = ''; | ||||||
|           if (Object.keys(found).length >= 10) { |           if (Object.keys(found).length >= 10) { | ||||||
|             return Object.keys(found); |             return Object.keys(found); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |       for (const vin of mp[tx].vin) { | ||||||
|  |         if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) { | ||||||
|  |           found[vin.prevout?.scriptpubkey_address] = ''; | ||||||
|  |           if (Object.keys(found).length >= 10) { | ||||||
|  |             return Object.keys(found); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     return Object.keys(found); |     return Object.keys(found); | ||||||
|   } |   } | ||||||
| @ -174,6 +188,14 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|     return this.bitcoindClient.sendRawTransaction(rawTransaction); |     return this.bitcoindClient.sendRawTransaction(rawTransaction); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> { | ||||||
|  |     if (rawTransactions.length) { | ||||||
|  |       return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined); | ||||||
|  |     } else { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { |   async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { | ||||||
|     const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); |     const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); | ||||||
|     return { |     return { | ||||||
| @ -224,6 +246,11 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|     return outspends; |     return outspends; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> { | ||||||
|  |     const txids = await this.$getTxIdsForBlock(blockhash); | ||||||
|  |     return this.$getRawTransaction(txids[0]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   $getEstimatedHashrate(blockHeight: number): Promise<number> { |   $getEstimatedHashrate(blockHeight: number): Promise<number> { | ||||||
|     // 120 is the default block span in Core
 |     // 120 is the default block span in Core
 | ||||||
|     return this.bitcoindClient.getNetworkHashPs(120, blockHeight); |     return this.bitcoindClient.getNetworkHashPs(120, blockHeight); | ||||||
| @ -296,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi { | |||||||
|       'witness_v1_taproot': 'v1_p2tr', |       'witness_v1_taproot': 'v1_p2tr', | ||||||
|       'nonstandard': 'nonstandard', |       'nonstandard': 'nonstandard', | ||||||
|       'multisig': 'multisig', |       'multisig': 'multisig', | ||||||
|  |       'anchor': 'anchor', | ||||||
|       'nulldata': 'op_return' |       'nulldata': 'op_return' | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import bitcoinClient from './bitcoin-client'; | |||||||
| import difficultyAdjustment from '../difficulty-adjustment'; | import difficultyAdjustment from '../difficulty-adjustment'; | ||||||
| import transactionRepository from '../../repositories/TransactionRepository'; | import transactionRepository from '../../repositories/TransactionRepository'; | ||||||
| import rbfCache from '../rbf-cache'; | import rbfCache from '../rbf-cache'; | ||||||
| import { calculateCpfp } from '../cpfp'; | import { calculateMempoolTxCpfp } from '../cpfp'; | ||||||
| import BlocksRepository from '../../repositories/BlocksRepository'; | import BlocksRepository from '../../repositories/BlocksRepository'; | ||||||
| 
 | 
 | ||||||
| class BitcoinRoutes { | class BitcoinRoutes { | ||||||
| @ -43,6 +43,7 @@ class BitcoinRoutes { | |||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) |       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) |       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) |       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) |       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) | ||||||
|       .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) |       .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) |       .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) | ||||||
| @ -56,6 +57,7 @@ class BitcoinRoutes { | |||||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) |           .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) | ||||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) |           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) | ||||||
|           .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) |           .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) | ||||||
|  |           .post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions) | ||||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) |           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) | ||||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) |           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) | ||||||
|           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) |           .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) | ||||||
| @ -238,13 +240,17 @@ class BitcoinRoutes { | |||||||
|           descendants: tx.descendants || null, |           descendants: tx.descendants || null, | ||||||
|           effectiveFeePerVsize: tx.effectiveFeePerVsize || null, |           effectiveFeePerVsize: tx.effectiveFeePerVsize || null, | ||||||
|           sigops: tx.sigops, |           sigops: tx.sigops, | ||||||
|  |           fee: tx.fee, | ||||||
|           adjustedVsize: tx.adjustedVsize, |           adjustedVsize: tx.adjustedVsize, | ||||||
|           acceleration: tx.acceleration |           acceleration: tx.acceleration, | ||||||
|  |           acceleratedBy: tx.acceleratedBy || undefined, | ||||||
|  |           acceleratedAt: tx.acceleratedAt || undefined, | ||||||
|  |           feeDelta: tx.feeDelta || undefined, | ||||||
|         }); |         }); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const cpfpInfo = calculateCpfp(tx, mempool.getMempool()); |       const cpfpInfo = calculateMempoolTxCpfp(tx, mempool.getMempool()); | ||||||
| 
 | 
 | ||||||
|       res.json(cpfpInfo); |       res.json(cpfpInfo); | ||||||
|       return; |       return; | ||||||
| @ -438,6 +444,20 @@ class BitcoinRoutes { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private async $getBlockTxAuditSummary(req: Request, res: Response) { | ||||||
|  |     try { | ||||||
|  |       const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); | ||||||
|  |       if (auditSummary) { | ||||||
|  |         res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); | ||||||
|  |         res.json(auditSummary); | ||||||
|  |       } else { | ||||||
|  |         return res.status(404).send(`transaction audit not available`); | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async getBlocks(req: Request, res: Response) { |   private async getBlocks(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 |       if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
 | ||||||
| @ -829,6 +849,19 @@ class BitcoinRoutes { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private async $testTransactions(req: Request, res: Response) { | ||||||
|  |     try { | ||||||
|  |       const rawTxs = Common.getTransactionsFromRequest(req); | ||||||
|  |       const maxfeerate = parseFloat(req.query.maxfeerate as string); | ||||||
|  |       const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); | ||||||
|  |       res.send(result); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       res.setHeader('content-type', 'text/plain'); | ||||||
|  |       res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) | ||||||
|  |         : (e.message || 'Error')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new BitcoinRoutes(); | export default new BitcoinRoutes(); | ||||||
|  | |||||||
| @ -54,7 +54,7 @@ export namespace IEsploraApi { | |||||||
|     scriptpubkey: string; |     scriptpubkey: string; | ||||||
|     scriptpubkey_asm: string; |     scriptpubkey_asm: string; | ||||||
|     scriptpubkey_type: string; |     scriptpubkey_type: string; | ||||||
|     scriptpubkey_address: string; |     scriptpubkey_address?: string; | ||||||
|     value: number; |     value: number; | ||||||
|     // Elements
 |     // Elements
 | ||||||
|     valuecommitment?: number; |     valuecommitment?: number; | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact | |||||||
| import { IEsploraApi } from './esplora-api.interface'; | import { IEsploraApi } from './esplora-api.interface'; | ||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| import { Common } from '../common'; | import { Common } from '../common'; | ||||||
|  | import { TestMempoolAcceptResult } from './bitcoin-api.interface'; | ||||||
| 
 | 
 | ||||||
| interface FailoverHost { | interface FailoverHost { | ||||||
|   host: string, |   host: string, | ||||||
| @ -24,6 +25,7 @@ interface FailoverHost { | |||||||
| class FailoverRouter { | class FailoverRouter { | ||||||
|   activeHost: FailoverHost; |   activeHost: FailoverHost; | ||||||
|   fallbackHost: FailoverHost; |   fallbackHost: FailoverHost; | ||||||
|  |   maxSlippage: number = config.ESPLORA.MAX_BEHIND_TIP ?? 2; | ||||||
|   maxHeight: number = 0; |   maxHeight: number = 0; | ||||||
|   hosts: FailoverHost[]; |   hosts: FailoverHost[]; | ||||||
|   multihost: boolean; |   multihost: boolean; | ||||||
| @ -92,13 +94,13 @@ class FailoverRouter { | |||||||
|         ); |         ); | ||||||
|         if (result) { |         if (result) { | ||||||
|           const height = result.data; |           const height = result.data; | ||||||
|           this.maxHeight = Math.max(height, this.maxHeight); |           host.latestHeight = height; | ||||||
|  |           this.maxHeight = Math.max(height || 0, ...this.hosts.map(h => (!(h.unreachable || h.timedOut || h.outOfSync) ? h.latestHeight || 0 : 0))); | ||||||
|           const rtt = result.config['meta'].rtt; |           const rtt = result.config['meta'].rtt; | ||||||
|           host.rtts.unshift(rtt); |           host.rtts.unshift(rtt); | ||||||
|           host.rtts.slice(0, 5); |           host.rtts.slice(0, 5); | ||||||
|           host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; |           host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; | ||||||
|           host.latestHeight = height; |           if (height == null || isNaN(height) || (this.maxHeight - height > this.maxSlippage)) { | ||||||
|           if (height == null || isNaN(height) || (this.maxHeight - height > 2)) { |  | ||||||
|             host.outOfSync = true; |             host.outOfSync = true; | ||||||
|           } else { |           } else { | ||||||
|             host.outOfSync = false; |             host.outOfSync = false; | ||||||
| @ -125,7 +127,6 @@ class FailoverRouter { | |||||||
|       host.checked = true; |       host.checked = true; | ||||||
|       host.lastChecked = Date.now(); |       host.lastChecked = Date.now(); | ||||||
| 
 | 
 | ||||||
|       // switch if the current host is out of sync or significantly slower than the next best alternative
 |  | ||||||
|       const rankOrder = this.sortHosts(); |       const rankOrder = this.sortHosts(); | ||||||
|       // switch if the current host is out of sync or significantly slower than the next best alternative
 |       // 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 !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) { |       if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== rankOrder[0] && rankOrder[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (rankOrder[0].rtt * 2) + 50)) { | ||||||
| @ -183,7 +184,6 @@ class FailoverRouter { | |||||||
| 
 | 
 | ||||||
|   // depose the active host and choose the next best replacement
 |   // depose the active host and choose the next best replacement
 | ||||||
|   private electHost(): void { |   private electHost(): void { | ||||||
|     this.activeHost.outOfSync = true; |  | ||||||
|     this.activeHost.failures = 0; |     this.activeHost.failures = 0; | ||||||
|     const rankOrder = this.sortHosts(); |     const rankOrder = this.sortHosts(); | ||||||
|     this.activeHost = rankOrder[0]; |     this.activeHost = rankOrder[0]; | ||||||
| @ -194,6 +194,7 @@ class FailoverRouter { | |||||||
|     host.failures++; |     host.failures++; | ||||||
|     if (host.failures > 5 && this.multihost) { |     if (host.failures > 5 && this.multihost) { | ||||||
|       logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`); |       logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`); | ||||||
|  |       this.activeHost.unreachable = true; | ||||||
|       this.electHost(); |       this.electHost(); | ||||||
|       return this.activeHost; |       return this.activeHost; | ||||||
|     } else { |     } else { | ||||||
| @ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi { | |||||||
|     throw new Error('Method not implemented.'); |     throw new Error('Method not implemented.'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]> { | ||||||
|  |     throw new Error('Method not implemented.'); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { |   $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { | ||||||
|     return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); |     return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); | ||||||
|   } |   } | ||||||
| @ -347,6 +352,11 @@ class ElectrsApi implements AbstractBitcoinApi { | |||||||
|     return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json'); |     return this.failoverRouter.$post<IEsploraApi.Outspend[]>('/internal/txs/outspends/by-outpoint', outpoints.map(out => `${out.txid}:${out.vout}`), 'json'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction> { | ||||||
|  |     const txid = await this.failoverRouter.$get<string>(`/block/${blockhash}/txid/0`); | ||||||
|  |     return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public startHealthChecks(): void { |   public startHealthChecks(): void { | ||||||
|     this.failoverRouter.startHealthChecks(); |     this.failoverRouter.startHealthChecks(); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import config from '../config'; | |||||||
| import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import memPool from './mempool'; | import memPool from './mempool'; | ||||||
| import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces'; | import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces'; | ||||||
| import { Common } from './common'; | import { Common } from './common'; | ||||||
| import diskCache from './disk-cache'; | import diskCache from './disk-cache'; | ||||||
| import transactionUtils from './transaction-utils'; | import transactionUtils from './transaction-utils'; | ||||||
| @ -29,6 +29,11 @@ import websocketHandler from './websocket-handler'; | |||||||
| import redisCache from './redis-cache'; | import redisCache from './redis-cache'; | ||||||
| import rbfCache from './rbf-cache'; | import rbfCache from './rbf-cache'; | ||||||
| import { calcBitsDifference } from './difficulty-adjustment'; | import { calcBitsDifference } from './difficulty-adjustment'; | ||||||
|  | import AccelerationRepository from '../repositories/AccelerationRepository'; | ||||||
|  | import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; | ||||||
|  | import mempool from './mempool'; | ||||||
|  | import CpfpRepository from '../repositories/CpfpRepository'; | ||||||
|  | import accelerationApi from './services/acceleration'; | ||||||
| 
 | 
 | ||||||
| class Blocks { | class Blocks { | ||||||
|   private blocks: BlockExtended[] = []; |   private blocks: BlockExtended[] = []; | ||||||
| @ -214,10 +219,10 @@ class Blocks { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { |   public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary { | ||||||
|     return { |     return { | ||||||
|       id: hash, |       id: hash, | ||||||
|       transactions: Common.classifyTransactions(transactions), |       transactions: Common.classifyTransactions(transactions, height), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -294,10 +299,12 @@ class Blocks { | |||||||
|     extras.virtualSize = block.weight / 4.0; |     extras.virtualSize = block.weight / 4.0; | ||||||
|     if (coinbaseTx?.vout.length > 0) { |     if (coinbaseTx?.vout.length > 0) { | ||||||
|       extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null; |       extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null; | ||||||
|  |       extras.coinbaseAddresses = [...new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[])]; | ||||||
|       extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null; |       extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null; | ||||||
|       extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null; |       extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null; | ||||||
|     } else { |     } else { | ||||||
|       extras.coinbaseAddress = null; |       extras.coinbaseAddress = null; | ||||||
|  |       extras.coinbaseAddresses = null; | ||||||
|       extras.coinbaseSignature = null; |       extras.coinbaseSignature = null; | ||||||
|       extras.coinbaseSignatureAscii = null; |       extras.coinbaseSignatureAscii = null; | ||||||
|     } |     } | ||||||
| @ -369,8 +376,7 @@ class Blocks { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig); |     const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter(address => address) as string[]; | ||||||
|     const addresses = txMinerInfo.vout.map((vout) => vout.scriptpubkey_address).filter((address) => address); |  | ||||||
| 
 | 
 | ||||||
|     let pools: PoolTag[] = []; |     let pools: PoolTag[] = []; | ||||||
|     if (config.DATABASE.ENABLED === true) { |     if (config.DATABASE.ENABLED === true) { | ||||||
| @ -379,26 +385,9 @@ class Blocks { | |||||||
|       pools = poolsParser.miningPools; |       pools = poolsParser.miningPools; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     for (let i = 0; i < pools.length; ++i) { |     const pool = poolsParser.matchBlockMiner(txMinerInfo.vin[0].scriptsig, addresses || [], pools); | ||||||
|       if (addresses.length) { |     if (pool) { | ||||||
|         const poolAddresses: string[] = typeof pools[i].addresses === 'string' ? |       return pool; | ||||||
|           JSON.parse(pools[i].addresses) : pools[i].addresses; |  | ||||||
|         for (let y = 0; y < poolAddresses.length; y++) { |  | ||||||
|           if (addresses.indexOf(poolAddresses[y]) !== -1) { |  | ||||||
|             return pools[i]; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const regexes: string[] = typeof pools[i].regexes === 'string' ? |  | ||||||
|         JSON.parse(pools[i].regexes) : pools[i].regexes; |  | ||||||
|       for (let y = 0; y < regexes.length; ++y) { |  | ||||||
|         const regex = new RegExp(regexes[y], 'i'); |  | ||||||
|         const match = asciiScriptSig.match(regex); |  | ||||||
|         if (match !== null) { |  | ||||||
|           return pools[i]; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (config.DATABASE.ENABLED === true) { |     if (config.DATABASE.ENABLED === true) { | ||||||
| @ -451,7 +440,7 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         if (config.MEMPOOL.BACKEND === 'esplora') { |         if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|           const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx)); |           const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx)); | ||||||
|           const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); |           const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); | ||||||
|           if (cpfpSummary) { |           if (cpfpSummary) { | ||||||
|             await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
 |             await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
 | ||||||
| @ -582,8 +571,11 @@ class Blocks { | |||||||
|     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); |     const blockchainInfo = await bitcoinClient.getBlockchainInfo(); | ||||||
|     const currentBlockHeight = blockchainInfo.blocks; |     const currentBlockHeight = blockchainInfo.blocks; | ||||||
| 
 | 
 | ||||||
|     const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesWithVersion(0); |     const targetSummaryVersion: number = 1; | ||||||
|     const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesWithVersion(0); |     const targetTemplateVersion: number = 1; | ||||||
|  | 
 | ||||||
|  |     const unclassifiedBlocksList = await BlocksSummariesRepository.$getSummariesBelowVersion(targetSummaryVersion); | ||||||
|  |     const unclassifiedTemplatesList = await BlocksSummariesRepository.$getTemplatesBelowVersion(targetTemplateVersion); | ||||||
| 
 | 
 | ||||||
|     // nothing to do
 |     // nothing to do
 | ||||||
|     if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { |     if (!unclassifiedBlocksList?.length && !unclassifiedTemplatesList?.length) { | ||||||
| @ -616,16 +608,24 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
|     for (let height = currentBlockHeight; height >= 0; height--) { |     for (let height = currentBlockHeight; height >= 0; height--) { | ||||||
|       try { |       try { | ||||||
|         let txs: TransactionExtended[] | null = null; |         let txs: MempoolTransactionExtended[] | null = null; | ||||||
|         if (unclassifiedBlocks[height]) { |         if (unclassifiedBlocks[height]) { | ||||||
|           const blockHash = unclassifiedBlocks[height]; |           const blockHash = unclassifiedBlocks[height]; | ||||||
|           // fetch transactions
 |           // fetch transactions
 | ||||||
|           txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendTransaction(tx)) || []; |           txs = (await bitcoinApi.$getTxsForBlock(blockHash)).map(tx => transactionUtils.extendMempoolTransaction(tx)) || []; | ||||||
|           // add CPFP
 |           // add CPFP
 | ||||||
|           const cpfpSummary = Common.calculateCpfp(height, txs, true); |           const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); | ||||||
|           // classify
 |           // classify
 | ||||||
|           const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); |           const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); | ||||||
|           await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 1); |           await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); | ||||||
|  |           if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { | ||||||
|  |             const cpfpClusters = await CpfpRepository.$getClustersAt(height); | ||||||
|  |             if (!cpfpRepository.compareClusters(cpfpClusters, cpfpSummary.clusters)) { | ||||||
|  |               // CPFP clusters changed - update the compact_cpfp tables
 | ||||||
|  |               await CpfpRepository.$deleteClustersAt(height); | ||||||
|  |               await this.$saveCpfp(blockHash, height, cpfpSummary); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|           await Common.sleep$(250); |           await Common.sleep$(250); | ||||||
|         } |         } | ||||||
|         if (unclassifiedTemplates[height]) { |         if (unclassifiedTemplates[height]) { | ||||||
| @ -651,9 +651,9 @@ class Blocks { | |||||||
|               } |               } | ||||||
|               templateTxs.push(tx || templateTx); |               templateTxs.push(tx || templateTx); | ||||||
|             } |             } | ||||||
|             const cpfpSummary = Common.calculateCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as TransactionExtended[], true); |             const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); | ||||||
|             // classify
 |             // classify
 | ||||||
|             const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); |             const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); | ||||||
|             const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; |             const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; | ||||||
|             for (const tx of classifiedTxs) { |             for (const tx of classifiedTxs) { | ||||||
|               classifiedTxMap[tx.txid] = tx; |               classifiedTxMap[tx.txid] = tx; | ||||||
| @ -689,6 +689,52 @@ class Blocks { | |||||||
|     this.classifyingBlocks = false; |     this.classifyingBlocks = false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * [INDEXING] Index missing coinbase addresses for all blocks | ||||||
|  |    */ | ||||||
|  |   public async $indexCoinbaseAddresses(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Get all indexed block hash
 | ||||||
|  |       const unindexedBlocks = await blocksRepository.$getBlocksWithoutCoinbaseAddresses(); | ||||||
|  | 
 | ||||||
|  |       if (!unindexedBlocks?.length) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logger.info(`Indexing missing coinbase addresses for ${unindexedBlocks.length} blocks`); | ||||||
|  | 
 | ||||||
|  |       // Logging
 | ||||||
|  |       let count = 0; | ||||||
|  |       let countThisRun = 0; | ||||||
|  |       let timer = Date.now() / 1000; | ||||||
|  |       const startedAt = Date.now() / 1000; | ||||||
|  |       for (const { height, hash } of unindexedBlocks) { | ||||||
|  |         // Logging
 | ||||||
|  |         const elapsedSeconds = (Date.now() / 1000) - timer; | ||||||
|  |         if (elapsedSeconds > 5) { | ||||||
|  |           const runningFor = (Date.now() / 1000) - startedAt; | ||||||
|  |           const blockPerSeconds = countThisRun / elapsedSeconds; | ||||||
|  |           const progress = Math.round(count / unindexedBlocks.length * 10000) / 100; | ||||||
|  |           logger.debug(`Indexing coinbase addresses for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`); | ||||||
|  |           timer = Date.now() / 1000; | ||||||
|  |           countThisRun = 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash); | ||||||
|  |         const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]); | ||||||
|  |         await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]); | ||||||
|  | 
 | ||||||
|  |         // Logging
 | ||||||
|  |         count++; | ||||||
|  |         countThisRun++; | ||||||
|  |       } | ||||||
|  |       logger.notice(`coinbase addresses indexing completed: indexed ${count} blocks`); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`coinbase addresses indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * [INDEXING] Index all blocks metadata for the mining dashboard |    * [INDEXING] Index all blocks metadata for the mining dashboard | ||||||
|    */ |    */ | ||||||
| @ -838,8 +884,11 @@ class Blocks { | |||||||
|       } else { |       } else { | ||||||
|         this.currentBlockHeight++; |         this.currentBlockHeight++; | ||||||
|         logger.debug(`New block found (#${this.currentBlockHeight})!`); |         logger.debug(`New block found (#${this.currentBlockHeight})!`); | ||||||
|         this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`); |         // skip updating the orphan block cache if we've fallen behind the chain tip
 | ||||||
|         await chainTips.updateOrphanedBlocks(); |         if (this.currentBlockHeight >= blockHeightTip - 2) { | ||||||
|  |           this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`); | ||||||
|  |           await chainTips.updateOrphanedBlocks(); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`); |       this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`); | ||||||
| @ -856,9 +905,14 @@ class Blocks { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); |       let accelerations = Object.values(mempool.getAccelerations()); | ||||||
|  |       if (accelerations?.length > 0) { | ||||||
|  |         const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0])); | ||||||
|  |         accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId)); | ||||||
|  |       } | ||||||
|  |       const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); | ||||||
|       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); |       const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); | ||||||
|       const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); |       const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions); | ||||||
|       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); |       this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); | ||||||
| 
 | 
 | ||||||
|       if (Common.indexingEnabled()) { |       if (Common.indexingEnabled()) { | ||||||
| @ -872,18 +926,19 @@ class Blocks { | |||||||
|             await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10); |             await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10); | ||||||
|             await HashratesRepository.$deleteLastEntries(); |             await HashratesRepository.$deleteLastEntries(); | ||||||
|             await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10); |             await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10); | ||||||
|  |             await AccelerationRepository.$deleteAccelerationsFrom(lastBlock.height - 10); | ||||||
|             this.blocks = this.blocks.slice(0, -10); |             this.blocks = this.blocks.slice(0, -10); | ||||||
|             this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`); |             this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`); | ||||||
|             for (let i = 10; i >= 0; --i) { |             for (let i = 10; i >= 0; --i) { | ||||||
|               const newBlock = await this.$indexBlock(lastBlock.height - i); |               const newBlock = await this.$indexBlock(lastBlock.height - i); | ||||||
|               this.blocks.push(newBlock); |               this.blocks.push(newBlock); | ||||||
|               this.updateTimerProgress(timer, `reindexed block`); |               this.updateTimerProgress(timer, `reindexed block`); | ||||||
|               let cpfpSummary; |               let newCpfpSummary; | ||||||
|               if (config.MEMPOOL.CPFP_INDEXING) { |               if (config.MEMPOOL.CPFP_INDEXING) { | ||||||
|                 cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i); |                 newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i); | ||||||
|                 this.updateTimerProgress(timer, `reindexed block cpfp`); |                 this.updateTimerProgress(timer, `reindexed block cpfp`); | ||||||
|               } |               } | ||||||
|               await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height); |               await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height); | ||||||
|               this.updateTimerProgress(timer, `reindexed block summary`); |               this.updateTimerProgress(timer, `reindexed block summary`); | ||||||
|             } |             } | ||||||
|             await mining.$indexDifficultyAdjustments(); |             await mining.$indexDifficultyAdjustments(); | ||||||
| @ -932,7 +987,7 @@ class Blocks { | |||||||
| 
 | 
 | ||||||
|       // start async callbacks
 |       // start async callbacks
 | ||||||
|       this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`); |       this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`); | ||||||
|       const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions)); |       const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions)); | ||||||
| 
 | 
 | ||||||
|       if (block.height % 2016 === 0) { |       if (block.height % 2016 === 0) { | ||||||
|         if (Common.indexingEnabled()) { |         if (Common.indexingEnabled()) { | ||||||
| @ -974,6 +1029,9 @@ class Blocks { | |||||||
|       if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { |       if (this.blocks.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { | ||||||
|         this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); |         this.blocks = this.blocks.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); | ||||||
|       } |       } | ||||||
|  |       blockSummary.transactions.forEach(tx => { | ||||||
|  |         delete tx.acc; | ||||||
|  |       }); | ||||||
|       this.blockSummaries.push(blockSummary); |       this.blockSummaries.push(blockSummary); | ||||||
|       if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { |       if (this.blockSummaries.length > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4) { | ||||||
|         this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); |         this.blockSummaries = this.blockSummaries.slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 4); | ||||||
| @ -1111,12 +1169,13 @@ class Blocks { | |||||||
|         transactions: cpfpSummary.transactions.map(tx => { |         transactions: cpfpSummary.transactions.map(tx => { | ||||||
|           let flags: number = 0; |           let flags: number = 0; | ||||||
|           try { |           try { | ||||||
|             flags = tx.flags || Common.getTransactionFlags(tx); |             flags = Common.getTransactionFlags(tx, height); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
|             logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); |             logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); | ||||||
|           } |           } | ||||||
|           return { |           return { | ||||||
|             txid: tx.txid, |             txid: tx.txid, | ||||||
|  |             time: tx.firstSeen, | ||||||
|             fee: tx.fee || 0, |             fee: tx.fee || 0, | ||||||
|             vsize: tx.vsize, |             vsize: tx.vsize, | ||||||
|             value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), |             value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), | ||||||
| @ -1125,11 +1184,11 @@ class Blocks { | |||||||
|           }; |           }; | ||||||
|         }), |         }), | ||||||
|       }; |       }; | ||||||
|       summaryVersion = 1; |       summaryVersion = cpfpSummary.version; | ||||||
|     } else { |     } else { | ||||||
|       if (config.MEMPOOL.BACKEND === 'esplora') { |       if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|         const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); |         const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); | ||||||
|         summary = this.summarizeBlockTransactions(hash, txs); |         summary = this.summarizeBlockTransactions(hash, height || 0, txs); | ||||||
|         summaryVersion = 1; |         summaryVersion = 1; | ||||||
|       } else { |       } else { | ||||||
|         // Call Core RPC
 |         // Call Core RPC
 | ||||||
| @ -1250,6 +1309,7 @@ class Blocks { | |||||||
|         utxoset_size: block.extras.utxoSetSize ?? null, |         utxoset_size: block.extras.utxoSetSize ?? null, | ||||||
|         coinbase_raw: block.extras.coinbaseRaw ?? null, |         coinbase_raw: block.extras.coinbaseRaw ?? null, | ||||||
|         coinbase_address: block.extras.coinbaseAddress ?? null, |         coinbase_address: block.extras.coinbaseAddress ?? null, | ||||||
|  |         coinbase_addresses: block.extras.coinbaseAddresses ?? null, | ||||||
|         coinbase_signature: block.extras.coinbaseSignature ?? null, |         coinbase_signature: block.extras.coinbaseSignature ?? null, | ||||||
|         coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null, |         coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null, | ||||||
|         pool_slug: block.extras.pool.slug ?? null, |         pool_slug: block.extras.pool.slug ?? null, | ||||||
| @ -1264,7 +1324,7 @@ class Blocks { | |||||||
|           let summaryVersion = 0; |           let summaryVersion = 0; | ||||||
|           if (config.MEMPOOL.BACKEND === 'esplora') { |           if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|             const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); |             const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); | ||||||
|             summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); |             summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs); | ||||||
|             summaryVersion = 1; |             summaryVersion = 1; | ||||||
|           } else { |           } else { | ||||||
|             // Call Core RPC
 |             // Call Core RPC
 | ||||||
| @ -1319,6 +1379,14 @@ class Blocks { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> { | ||||||
|  |     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { | ||||||
|  |       return BlocksAuditsRepository.$getBlockTxAudit(hash, txid); | ||||||
|  |     } else { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public getLastDifficultyAdjustmentTime(): number { |   public getLastDifficultyAdjustmentTime(): number { | ||||||
|     return this.lastDifficultyAdjustmentTime; |     return this.lastDifficultyAdjustmentTime; | ||||||
|   } |   } | ||||||
| @ -1335,11 +1403,11 @@ class Blocks { | |||||||
|     return this.currentBlockHeight; |     return this.currentBlockHeight; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> { |   public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> { | ||||||
|     let transactions = txs; |     let transactions = txs; | ||||||
|     if (!transactions) { |     if (!transactions) { | ||||||
|       if (config.MEMPOOL.BACKEND === 'esplora') { |       if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|         transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); |         transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx)); | ||||||
|       } |       } | ||||||
|       if (!transactions) { |       if (!transactions) { | ||||||
|         const block = await bitcoinClient.getBlock(hash, 2); |         const block = await bitcoinClient.getBlock(hash, 2); | ||||||
| @ -1351,7 +1419,7 @@ class Blocks { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (transactions?.length != null) { |     if (transactions?.length != null) { | ||||||
|       const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]); |       const summary = calculateFastBlockCpfp(height, transactions); | ||||||
| 
 | 
 | ||||||
|       await this.$saveCpfp(hash, height, summary); |       await this.$saveCpfp(hash, height, summary); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,32 +12,68 @@ export interface OrphanedBlock { | |||||||
|   height: number; |   height: number; | ||||||
|   hash: string; |   hash: string; | ||||||
|   status: 'valid-fork' | 'valid-headers' | 'headers-only'; |   status: 'valid-fork' | 'valid-headers' | 'headers-only'; | ||||||
|  |   prevhash: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class ChainTips { | class ChainTips { | ||||||
|   private chainTips: ChainTip[] = []; |   private chainTips: ChainTip[] = []; | ||||||
|   private orphanedBlocks: OrphanedBlock[] = []; |   private orphanedBlocks: { [hash: string]: OrphanedBlock } = {}; | ||||||
|  |   private blockCache: { [hash: string]: OrphanedBlock } = {}; | ||||||
|  |   private orphansByHeight: { [height: number]: OrphanedBlock[] } = {}; | ||||||
| 
 | 
 | ||||||
|   public async updateOrphanedBlocks(): Promise<void> { |   public async updateOrphanedBlocks(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       this.chainTips = await bitcoinClient.getChainTips(); |       this.chainTips = await bitcoinClient.getChainTips(); | ||||||
|       this.orphanedBlocks = []; | 
 | ||||||
|  |       const start = Date.now(); | ||||||
|  |       const breakAt = start + 10000; | ||||||
|  |       let newOrphans = 0; | ||||||
|  |       this.orphanedBlocks = {}; | ||||||
| 
 | 
 | ||||||
|       for (const chain of this.chainTips) { |       for (const chain of this.chainTips) { | ||||||
|         if (chain.status === 'valid-fork' || chain.status === 'valid-headers') { |         if (chain.status === 'valid-fork' || chain.status === 'valid-headers') { | ||||||
|           let block = await bitcoinClient.getBlock(chain.hash); |           const orphans: OrphanedBlock[] = []; | ||||||
|           while (block && block.confirmations === -1) { |           let hash = chain.hash; | ||||||
|             this.orphanedBlocks.push({ |           do { | ||||||
|               height: block.height, |             let orphan = this.blockCache[hash]; | ||||||
|               hash: block.hash, |             if (!orphan) { | ||||||
|               status: chain.status |               const block = await bitcoinClient.getBlock(hash); | ||||||
|             }); |               if (block && block.confirmations === -1) { | ||||||
|             block = await bitcoinClient.getBlock(block.previousblockhash); |                 newOrphans++; | ||||||
|  |                 orphan = { | ||||||
|  |                   height: block.height, | ||||||
|  |                   hash: block.hash, | ||||||
|  |                   status: chain.status, | ||||||
|  |                   prevhash: block.previousblockhash, | ||||||
|  |                 }; | ||||||
|  |                 this.blockCache[hash] = orphan; | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             if (orphan) { | ||||||
|  |               orphans.push(orphan); | ||||||
|  |             } | ||||||
|  |             hash = orphan?.prevhash; | ||||||
|  |           } while (hash && (Date.now() < breakAt)); | ||||||
|  |           for (const orphan of orphans) { | ||||||
|  |             this.orphanedBlocks[orphan.hash] = orphan; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |         if (Date.now() >= breakAt) { | ||||||
|  |           logger.debug(`Breaking orphaned blocks updater after 10s, will continue next block`); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`); |       this.orphansByHeight = {}; | ||||||
|  |       const allOrphans = Object.values(this.orphanedBlocks); | ||||||
|  |       for (const orphan of allOrphans) { | ||||||
|  |         if (!this.orphansByHeight[orphan.height]) { | ||||||
|  |           this.orphansByHeight[orphan.height] = []; | ||||||
|  |         } | ||||||
|  |         this.orphansByHeight[orphan.height].push(orphan); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logger.debug(`Updated orphaned blocks cache. Fetched ${newOrphans} new orphaned blocks. Total ${allOrphans.length}`); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`); |       logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`); | ||||||
|     } |     } | ||||||
| @ -48,13 +84,7 @@ class ChainTips { | |||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const orphans: OrphanedBlock[] = []; |     return this.orphansByHeight[height] || []; | ||||||
|     for (const block of this.orphanedBlocks) { |  | ||||||
|       if (block.height === height) { |  | ||||||
|         orphans.push(block); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return orphans; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import * as bitcoinjs from 'bitcoinjs-lib'; | import * as bitcoinjs from 'bitcoinjs-lib'; | ||||||
| import { Request } from 'express'; | import { Request } from 'express'; | ||||||
| import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; | import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import { NodeSocket } from '../repositories/NodesSocketsRepository'; | import { NodeSocket } from '../repositories/NodesSocketsRepository'; | ||||||
| import { isIP } from 'net'; | import { isIP } from 'net'; | ||||||
| @ -10,7 +10,6 @@ import logger from '../logger'; | |||||||
| import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; | import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; | ||||||
| 
 | 
 | ||||||
| // Bitcoin Core default policy settings
 | // Bitcoin Core default policy settings
 | ||||||
| const TX_MAX_STANDARD_VERSION = 2; |  | ||||||
| const MAX_STANDARD_TX_WEIGHT = 400_000; | const MAX_STANDARD_TX_WEIGHT = 400_000; | ||||||
| const MAX_BLOCK_SIGOPS_COST = 80_000; | const MAX_BLOCK_SIGOPS_COST = 80_000; | ||||||
| const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); | const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); | ||||||
| @ -200,10 +199,13 @@ export class Common { | |||||||
|    * |    * | ||||||
|    * returns true early if any standardness rule is violated, otherwise false |    * returns true early if any standardness rule is violated, otherwise false | ||||||
|    * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) |    * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) | ||||||
|  |    * | ||||||
|  |    * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks. | ||||||
|  |    * For now, just pull out individual rules into versioned functions where necessary. | ||||||
|    */ |    */ | ||||||
|   static isNonStandard(tx: TransactionExtended): boolean { |   static isNonStandard(tx: TransactionExtended, height?: number): boolean { | ||||||
|     // version
 |     // version
 | ||||||
|     if (tx.version > TX_MAX_STANDARD_VERSION) { |     if (this.isNonStandardVersion(tx, height)) { | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -250,6 +252,8 @@ export class Common { | |||||||
|         } |         } | ||||||
|       } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { |       } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { | ||||||
|         return true; |         return true; | ||||||
|  |       } else if (this.isNonStandardAnchor(tx, height)) { | ||||||
|  |         return true; | ||||||
|       } |       } | ||||||
|       // TODO: bad-witness-nonstandard
 |       // TODO: bad-witness-nonstandard
 | ||||||
|     } |     } | ||||||
| @ -258,9 +262,15 @@ export class Common { | |||||||
|     let opreturnCount = 0; |     let opreturnCount = 0; | ||||||
|     for (const vout of tx.vout) { |     for (const vout of tx.vout) { | ||||||
|       // scriptpubkey
 |       // scriptpubkey
 | ||||||
|       if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) { |       if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) { | ||||||
|         // (non-standard output type)
 |         // (non-standard output type)
 | ||||||
|         return true; |         return true; | ||||||
|  |       } else if (vout.scriptpubkey_type === 'unknown') { | ||||||
|  |         // undefined segwit version/length combinations are actually standard in outputs
 | ||||||
|  |         // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
 | ||||||
|  |         if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) { | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|       } else if (vout.scriptpubkey_type === 'multisig') { |       } else if (vout.scriptpubkey_type === 'multisig') { | ||||||
|         if (!DEFAULT_PERMIT_BAREMULTISIG) { |         if (!DEFAULT_PERMIT_BAREMULTISIG) { | ||||||
|           // bare-multisig
 |           // bare-multisig
 | ||||||
| @ -286,7 +296,7 @@ export class Common { | |||||||
|         dustSize += getVarIntLength(dustSize); |         dustSize += getVarIntLength(dustSize); | ||||||
|         // add value size
 |         // add value size
 | ||||||
|         dustSize += 8; |         dustSize += 8; | ||||||
|         if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) { |         if (Common.isWitnessProgram(vout.scriptpubkey)) { | ||||||
|           dustSize += 67; |           dustSize += 67; | ||||||
|         } else { |         } else { | ||||||
|           dustSize += 148; |           dustSize += 148; | ||||||
| @ -308,6 +318,70 @@ export class Common { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
 | ||||||
|  |   // followed by a data push between 2 and 40 bytes.
 | ||||||
|  |   // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
 | ||||||
|  |   static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } { | ||||||
|  |     if (scriptpubkey.length < 8 || scriptpubkey.length > 84) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     const version = parseInt(scriptpubkey.slice(0,2), 16); | ||||||
|  |     if (version !== 0 && version < 0x51 || version > 0x60) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     const push = parseInt(scriptpubkey.slice(2,4), 16); | ||||||
|  |     if (push + 2 === (scriptpubkey.length / 2)) { | ||||||
|  |       return { | ||||||
|  |         version: version ? version - 0x50 : 0, | ||||||
|  |         program: scriptpubkey.slice(4), | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Individual versioned standardness rules
 | ||||||
|  | 
 | ||||||
|  |   static V3_STANDARDNESS_ACTIVATION_HEIGHT = { | ||||||
|  |     'testnet4': 42_000, | ||||||
|  |     'testnet': 2_900_000, | ||||||
|  |     'signet': 211_000, | ||||||
|  |     '': 863_500, | ||||||
|  |   }; | ||||||
|  |   static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean { | ||||||
|  |     let TX_MAX_STANDARD_VERSION = 3; | ||||||
|  |     if ( | ||||||
|  |       height != null | ||||||
|  |       && this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] | ||||||
|  |       && height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] | ||||||
|  |     ) { | ||||||
|  |       // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
 | ||||||
|  |       TX_MAX_STANDARD_VERSION = 2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (tx.version > TX_MAX_STANDARD_VERSION) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = { | ||||||
|  |     'testnet4': 42_000, | ||||||
|  |     'testnet': 2_900_000, | ||||||
|  |     'signet': 211_000, | ||||||
|  |     '': 863_500, | ||||||
|  |   }; | ||||||
|  |   static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean { | ||||||
|  |     if ( | ||||||
|  |       height != null | ||||||
|  |       && this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] | ||||||
|  |       && height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] | ||||||
|  |     ) { | ||||||
|  |       // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
 | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static getNonWitnessSize(tx: TransactionExtended): number { |   static getNonWitnessSize(tx: TransactionExtended): number { | ||||||
|     let weight = tx.weight; |     let weight = tx.weight; | ||||||
|     let hasWitness = false; |     let hasWitness = false; | ||||||
| @ -388,16 +462,19 @@ export class Common { | |||||||
|     return flags; |     return flags; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static getTransactionFlags(tx: TransactionExtended): number { |   static getTransactionFlags(tx: TransactionExtended, height?: number): number { | ||||||
|     let flags = tx.flags ? BigInt(tx.flags) : 0n; |     let flags = tx.flags ? BigInt(tx.flags) : 0n; | ||||||
| 
 | 
 | ||||||
|     // Update variable flags (CPFP, RBF)
 |     // Update variable flags (CPFP, RBF)
 | ||||||
|  |     flags &= ~TransactionFlags.cpfp_child; | ||||||
|     if (tx.ancestors?.length) { |     if (tx.ancestors?.length) { | ||||||
|       flags |= TransactionFlags.cpfp_child; |       flags |= TransactionFlags.cpfp_child; | ||||||
|     } |     } | ||||||
|  |     flags &= ~TransactionFlags.cpfp_parent; | ||||||
|     if (tx.descendants?.length) { |     if (tx.descendants?.length) { | ||||||
|       flags |= TransactionFlags.cpfp_parent; |       flags |= TransactionFlags.cpfp_parent; | ||||||
|     } |     } | ||||||
|  |     flags &= ~TransactionFlags.replacement; | ||||||
|     if (tx.replacement) { |     if (tx.replacement) { | ||||||
|       flags |= TransactionFlags.replacement; |       flags |= TransactionFlags.replacement; | ||||||
|     } |     } | ||||||
| @ -433,11 +510,10 @@ export class Common { | |||||||
|           case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; |           case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; | ||||||
|           case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; |           case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; | ||||||
|           case 'v1_p2tr': { |           case 'v1_p2tr': { | ||||||
|             if (!vin.witness?.length) { |  | ||||||
|               throw new Error('Taproot input missing witness data'); |  | ||||||
|             } |  | ||||||
|             flags |= TransactionFlags.p2tr; |             flags |= TransactionFlags.p2tr; | ||||||
|             flags = Common.isInscription(vin, flags); |             if (vin.witness?.length) { | ||||||
|  |               flags = Common.isInscription(vin, flags); | ||||||
|  |             } | ||||||
|           } break; |           } break; | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
| @ -519,7 +595,7 @@ export class Common { | |||||||
|     if (hasFakePubkey) { |     if (hasFakePubkey) { | ||||||
|       flags |= TransactionFlags.fake_pubkey; |       flags |= TransactionFlags.fake_pubkey; | ||||||
|     } |     } | ||||||
|      | 
 | ||||||
|     // fast but bad heuristic to detect possible coinjoins
 |     // fast but bad heuristic to detect possible coinjoins
 | ||||||
|     // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
 |     // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
 | ||||||
|     const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; |     const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; | ||||||
| @ -535,17 +611,17 @@ export class Common { | |||||||
|       flags |= TransactionFlags.batch_payout; |       flags |= TransactionFlags.batch_payout; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (this.isNonStandard(tx)) { |     if (this.isNonStandard(tx, height)) { | ||||||
|       flags |= TransactionFlags.nonstandard; |       flags |= TransactionFlags.nonstandard; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return Number(flags); |     return Number(flags); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static classifyTransaction(tx: TransactionExtended): TransactionClassified { |   static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified { | ||||||
|     let flags = 0; |     let flags = 0; | ||||||
|     try { |     try { | ||||||
|       flags = Common.getTransactionFlags(tx); |       flags = Common.getTransactionFlags(tx, height); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); |       logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); | ||||||
|     } |     } | ||||||
| @ -556,8 +632,8 @@ export class Common { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { |   static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] { | ||||||
|     return txs.map(Common.classifyTransaction); |     return txs.map(tx => Common.classifyTransaction(tx, height)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static stripTransaction(tx: TransactionExtended): TransactionStripped { |   static stripTransaction(tx: TransactionExtended): TransactionStripped { | ||||||
| @ -780,96 +856,6 @@ export class Common { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static calculateCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary { |  | ||||||
|     const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
 |  | ||||||
|     const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
 |  | ||||||
|     let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
 |  | ||||||
|     let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
 |  | ||||||
|     const txMap: { [txid: string]: TransactionExtended } = {}; |  | ||||||
|     // initialize the txMap
 |  | ||||||
|     for (const tx of transactions) { |  | ||||||
|       txMap[tx.txid] = tx; |  | ||||||
|     } |  | ||||||
|     // reverse pass to identify CPFP clusters
 |  | ||||||
|     for (let i = transactions.length - 1; i >= 0; i--) { |  | ||||||
|       const tx = transactions[i]; |  | ||||||
|       if (!ancestors[tx.txid]) { |  | ||||||
|         let totalFee = 0; |  | ||||||
|         let totalVSize = 0; |  | ||||||
|         clusterTxs.forEach(tx => { |  | ||||||
|           totalFee += tx?.fee || 0; |  | ||||||
|           totalVSize += (tx.weight / 4); |  | ||||||
|         }); |  | ||||||
|         const effectiveFeePerVsize = totalFee / totalVSize; |  | ||||||
|         let cluster: CpfpCluster; |  | ||||||
|         if (clusterTxs.length > 1) { |  | ||||||
|           cluster = { |  | ||||||
|             root: clusterTxs[0].txid, |  | ||||||
|             height, |  | ||||||
|             txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), |  | ||||||
|             effectiveFeePerVsize, |  | ||||||
|           }; |  | ||||||
|           clusters.push(cluster); |  | ||||||
|         } |  | ||||||
|         clusterTxs.forEach(tx => { |  | ||||||
|           txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; |  | ||||||
|           if (cluster) { |  | ||||||
|             clusterMap[tx.txid] = cluster; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|         // reset working vars
 |  | ||||||
|         clusterTxs = []; |  | ||||||
|         ancestors = {}; |  | ||||||
|       } |  | ||||||
|       clusterTxs.push(tx); |  | ||||||
|       tx.vin.forEach(vin => { |  | ||||||
|         ancestors[vin.txid] = true; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     // forward pass to enforce ancestor rate caps
 |  | ||||||
|     for (const tx of transactions) { |  | ||||||
|       let minAncestorRate = tx.effectiveFeePerVsize; |  | ||||||
|       for (const vin of tx.vin) { |  | ||||||
|         if (txMap[vin.txid]?.effectiveFeePerVsize) { |  | ||||||
|           minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       // check rounded values to skip cases with almost identical fees
 |  | ||||||
|       const roundedMinAncestorRate = Math.ceil(minAncestorRate); |  | ||||||
|       const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize); |  | ||||||
|       if (roundedMinAncestorRate < roundedEffectiveFeeRate) { |  | ||||||
|         tx.effectiveFeePerVsize = minAncestorRate; |  | ||||||
|         if (!clusterMap[tx.txid]) { |  | ||||||
|           // add a single-tx cluster to record the dependent rate
 |  | ||||||
|           const cluster = { |  | ||||||
|             root: tx.txid, |  | ||||||
|             height, |  | ||||||
|             txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }], |  | ||||||
|             effectiveFeePerVsize: minAncestorRate, |  | ||||||
|           }; |  | ||||||
|           clusterMap[tx.txid] = cluster; |  | ||||||
|           clusters.push(cluster); |  | ||||||
|         } else { |  | ||||||
|           // update the existing cluster with the dependent rate
 |  | ||||||
|           clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     if (saveRelatives) { |  | ||||||
|       for (const cluster of clusters) { |  | ||||||
|         cluster.txs.forEach((member, index) => { |  | ||||||
|           txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse(); |  | ||||||
|           txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse(); |  | ||||||
|           txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return { |  | ||||||
|       transactions, |  | ||||||
|       clusters, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats { |   static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats { | ||||||
|     const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate); |     const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate); | ||||||
| 
 | 
 | ||||||
| @ -877,9 +863,10 @@ export class Common { | |||||||
|     let medianFee = 0; |     let medianFee = 0; | ||||||
|     let medianWeight = 0; |     let medianWeight = 0; | ||||||
| 
 | 
 | ||||||
|     // calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
 |     // calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
 | ||||||
|     const leftBound = 1995000; |     const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800; | ||||||
|     const rightBound = 2005000; |     const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth); | ||||||
|  |     const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth); | ||||||
|     for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) { |     for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) { | ||||||
|       const left = weightCount; |       const left = weightCount; | ||||||
|       const right = weightCount + sortedTxs[i].weight; |       const right = weightCount + sortedTxs[i].weight; | ||||||
| @ -946,6 +933,33 @@ export class Common { | |||||||
|     return this.validateTransactionHex(matches[1].toLowerCase()); |     return this.validateTransactionHex(matches[1].toLowerCase()); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   static getTransactionsFromRequest(req: Request, limit: number = 25): string[] { | ||||||
|  |     if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) { | ||||||
|  |       throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (limit && req.body.length > limit) { | ||||||
|  |       throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const txs = req.body; | ||||||
|  | 
 | ||||||
|  |     return txs.map(rawTx => { | ||||||
|  |       // Support both upper and lower case hex
 | ||||||
|  |       // Support both txHash= Form and direct API POST
 | ||||||
|  |       const reg = /^((?:[a-fA-F0-9]{2})+)$/; | ||||||
|  |       const matches = reg.exec(rawTx); | ||||||
|  |       if (!matches || !matches[1]) { | ||||||
|  |         throw Object.assign(new Error('Invalid hex string'), { code: -2 }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Guaranteed to be a hex string of multiple of 2
 | ||||||
|  |       // Guaranteed to be lower case
 | ||||||
|  |       // Guaranteed to pass validation (see function below)
 | ||||||
|  |       return this.validateTransactionHex(matches[1].toLowerCase()); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private static validateTransactionHex(txhex: string): string { |   private static validateTransactionHex(txhex: string): string { | ||||||
|     // Do not mutate txhex
 |     // Do not mutate txhex
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,29 +1,174 @@ | |||||||
| import { CpfpInfo, MempoolTransactionExtended } from '../mempool.interfaces'; | import { Ancestor, CpfpCluster, CpfpInfo, CpfpSummary, MempoolTransactionExtended, TransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import { GraphTx, convertToGraphTx, expandRelativesGraph, initializeRelatives, makeBlockTemplate, mempoolComparator, removeAncestors, setAncestorScores } from './mini-miner'; | ||||||
| import memPool from './mempool'; | import memPool from './mempool'; | ||||||
|  | import { Acceleration } from './acceleration/acceleration'; | ||||||
| 
 | 
 | ||||||
| const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
 | const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
 | ||||||
| const MAX_GRAPH_SIZE = 50; // the maximum number of in-mempool relatives to consider
 | const MAX_CLUSTER_ITERATIONS = 100; | ||||||
| 
 | 
 | ||||||
| interface GraphTx extends MempoolTransactionExtended { | export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary { | ||||||
|   depends: string[]; |   const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
 | ||||||
|   spentby: string[]; |   const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
 | ||||||
|   ancestorMap: Map<string, GraphTx>; |   let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
 | ||||||
|   fees: { |   let ancestors: { [txid: string]: boolean } = {}; // working set of ancestors of the current cluster root
 | ||||||
|     base: number; |   const txMap: { [txid: string]: TransactionExtended } = {}; | ||||||
|     ancestor: number; |   // initialize the txMap
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     txMap[tx.txid] = tx; | ||||||
|  |   } | ||||||
|  |   // reverse pass to identify CPFP clusters
 | ||||||
|  |   for (let i = transactions.length - 1; i >= 0; i--) { | ||||||
|  |     const tx = transactions[i]; | ||||||
|  |     if (!ancestors[tx.txid]) { | ||||||
|  |       let totalFee = 0; | ||||||
|  |       let totalVSize = 0; | ||||||
|  |       clusterTxs.forEach(tx => { | ||||||
|  |         totalFee += tx?.fee || 0; | ||||||
|  |         totalVSize += (tx.weight / 4); | ||||||
|  |       }); | ||||||
|  |       const effectiveFeePerVsize = totalFee / totalVSize; | ||||||
|  |       let cluster: CpfpCluster; | ||||||
|  |       if (clusterTxs.length > 1) { | ||||||
|  |         cluster = { | ||||||
|  |           root: clusterTxs[0].txid, | ||||||
|  |           height, | ||||||
|  |           txs: clusterTxs.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }), | ||||||
|  |           effectiveFeePerVsize, | ||||||
|  |         }; | ||||||
|  |         clusters.push(cluster); | ||||||
|  |       } | ||||||
|  |       clusterTxs.forEach(tx => { | ||||||
|  |         txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; | ||||||
|  |         if (cluster) { | ||||||
|  |           clusterMap[tx.txid] = cluster; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       // reset working vars
 | ||||||
|  |       clusterTxs = []; | ||||||
|  |       ancestors = {}; | ||||||
|  |     } | ||||||
|  |     clusterTxs.push(tx); | ||||||
|  |     tx.vin.forEach(vin => { | ||||||
|  |       ancestors[vin.txid] = true; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   // forward pass to enforce ancestor rate caps
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     let minAncestorRate = tx.effectiveFeePerVsize; | ||||||
|  |     for (const vin of tx.vin) { | ||||||
|  |       if (txMap[vin.txid]?.effectiveFeePerVsize) { | ||||||
|  |         minAncestorRate = Math.min(minAncestorRate, txMap[vin.txid].effectiveFeePerVsize); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // check rounded values to skip cases with almost identical fees
 | ||||||
|  |     const roundedMinAncestorRate = Math.ceil(minAncestorRate); | ||||||
|  |     const roundedEffectiveFeeRate = Math.floor(tx.effectiveFeePerVsize); | ||||||
|  |     if (roundedMinAncestorRate < roundedEffectiveFeeRate) { | ||||||
|  |       tx.effectiveFeePerVsize = minAncestorRate; | ||||||
|  |       if (!clusterMap[tx.txid]) { | ||||||
|  |         // add a single-tx cluster to record the dependent rate
 | ||||||
|  |         const cluster = { | ||||||
|  |           root: tx.txid, | ||||||
|  |           height, | ||||||
|  |           txs: [{ txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }], | ||||||
|  |           effectiveFeePerVsize: minAncestorRate, | ||||||
|  |         }; | ||||||
|  |         clusterMap[tx.txid] = cluster; | ||||||
|  |         clusters.push(cluster); | ||||||
|  |       } else { | ||||||
|  |         // update the existing cluster with the dependent rate
 | ||||||
|  |         clusterMap[tx.txid].effectiveFeePerVsize = minAncestorRate; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (saveRelatives) { | ||||||
|  |     for (const cluster of clusters) { | ||||||
|  |       cluster.txs.forEach((member, index) => { | ||||||
|  |         txMap[member.txid].descendants = cluster.txs.slice(0, index).reverse(); | ||||||
|  |         txMap[member.txid].ancestors = cluster.txs.slice(index + 1).reverse(); | ||||||
|  |         txMap[member.txid].effectiveFeePerVsize = cluster.effectiveFeePerVsize; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return { | ||||||
|  |     transactions, | ||||||
|  |     clusters, | ||||||
|  |     version: 1, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function calculateGoodBlockCpfp(height: number, transactions: MempoolTransactionExtended[], accelerations: Acceleration[]): CpfpSummary { | ||||||
|  |   const txMap: { [txid: string]: MempoolTransactionExtended } = {}; | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     txMap[tx.txid] = tx; | ||||||
|  |   } | ||||||
|  |   const template = makeBlockTemplate(transactions, accelerations, 1, Infinity, Infinity); | ||||||
|  |   const clusters = new Map<string, string[]>(); | ||||||
|  |   for (const tx of template) { | ||||||
|  |     const cluster = tx.cluster || []; | ||||||
|  |     const root = cluster.length ? cluster[cluster.length - 1] : null; | ||||||
|  |     if (cluster.length > 1 && root && !clusters.has(root)) { | ||||||
|  |       clusters.set(root, cluster); | ||||||
|  |     } | ||||||
|  |     txMap[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const clusterArray: CpfpCluster[] = []; | ||||||
|  | 
 | ||||||
|  |   for (const cluster of clusters.values()) { | ||||||
|  |     for (const txid of cluster) { | ||||||
|  |       const mempoolTx = txMap[txid]; | ||||||
|  |       if (mempoolTx) { | ||||||
|  |         const ancestors: Ancestor[] = []; | ||||||
|  |         const descendants: Ancestor[] = []; | ||||||
|  |         let matched = false; | ||||||
|  |         cluster.forEach(relativeTxid => { | ||||||
|  |           if (relativeTxid === txid) { | ||||||
|  |             matched = true; | ||||||
|  |           } else { | ||||||
|  |             const relative = { | ||||||
|  |               txid: relativeTxid, | ||||||
|  |               fee: txMap[relativeTxid].fee, | ||||||
|  |               weight: (txMap[relativeTxid].adjustedVsize * 4) || txMap[relativeTxid].weight, | ||||||
|  |             }; | ||||||
|  |             if (matched) { | ||||||
|  |               descendants.push(relative); | ||||||
|  |             } else { | ||||||
|  |               ancestors.push(relative); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |         if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { | ||||||
|  |           mempoolTx.cpfpDirty = true; | ||||||
|  |         } | ||||||
|  |         Object.assign(mempoolTx, { ancestors, descendants, bestDescendant: null, cpfpChecked: true }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const root = cluster[cluster.length - 1]; | ||||||
|  |     clusterArray.push({ | ||||||
|  |       root: root, | ||||||
|  |       height, | ||||||
|  |       txs: cluster.reverse().map(txid => ({ | ||||||
|  |         txid, | ||||||
|  |         fee: txMap[txid].fee, | ||||||
|  |         weight: (txMap[txid].adjustedVsize * 4) || txMap[txid].weight, | ||||||
|  |       })), | ||||||
|  |       effectiveFeePerVsize: txMap[root].effectiveFeePerVsize, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     transactions: transactions.map(tx => txMap[tx.txid]), | ||||||
|  |     clusters: clusterArray, | ||||||
|  |     version: 2, | ||||||
|   }; |   }; | ||||||
|   ancestorcount: number; |  | ||||||
|   ancestorsize: number; |  | ||||||
|   ancestorRate: number; |  | ||||||
|   individualRate: number; |  | ||||||
|   score: number; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for |  * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for | ||||||
|  * that transaction (and all others in the same cluster) |  * that transaction (and all others in the same cluster) | ||||||
|  */ |  */ | ||||||
| export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { | export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { | ||||||
|   if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { |   if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { | ||||||
|     tx.cpfpDirty = false; |     tx.cpfpDirty = false; | ||||||
|     return { |     return { | ||||||
| @ -32,30 +177,31 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: | |||||||
|       descendants: tx.descendants || [], |       descendants: tx.descendants || [], | ||||||
|       effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, |       effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, | ||||||
|       sigops: tx.sigops, |       sigops: tx.sigops, | ||||||
|  |       fee: tx.fee, | ||||||
|       adjustedVsize: tx.adjustedVsize, |       adjustedVsize: tx.adjustedVsize, | ||||||
|       acceleration: tx.acceleration |       acceleration: tx.acceleration | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const ancestorMap = new Map<string, GraphTx>(); |   const ancestorMap = new Map<string, GraphTx>(); | ||||||
|   const graphTx = mempoolToGraphTx(tx); |   const graphTx = convertToGraphTx(tx, memPool.getSpendMap()); | ||||||
|   ancestorMap.set(tx.txid, graphTx); |   ancestorMap.set(tx.txid, graphTx); | ||||||
| 
 | 
 | ||||||
|   const allRelatives = expandRelativesGraph(mempool, ancestorMap); |   const allRelatives = expandRelativesGraph(mempool, ancestorMap, memPool.getSpendMap()); | ||||||
|   const relativesMap = initializeRelatives(allRelatives); |   const relativesMap = initializeRelatives(allRelatives); | ||||||
|   const cluster = calculateCpfpCluster(tx.txid, relativesMap); |   const cluster = calculateCpfpCluster(tx.txid, relativesMap); | ||||||
| 
 | 
 | ||||||
|   let totalVsize = 0; |   let totalVsize = 0; | ||||||
|   let totalFee = 0; |   let totalFee = 0; | ||||||
|   for (const tx of cluster.values()) { |   for (const tx of cluster.values()) { | ||||||
|     totalVsize += tx.adjustedVsize; |     totalVsize += tx.vsize; | ||||||
|     totalFee += tx.fee; |     totalFee += tx.fees.base; | ||||||
|   } |   } | ||||||
|   const effectiveFeePerVsize = totalFee / totalVsize; |   const effectiveFeePerVsize = totalFee / totalVsize; | ||||||
|   for (const tx of cluster.values()) { |   for (const tx of cluster.values()) { | ||||||
|     mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; |     mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; | ||||||
|     mempool[tx.txid].ancestors = Array.from(tx.ancestorMap.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee })); |     mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); | ||||||
|     mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestorMap.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fee })); |     mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); | ||||||
|     mempool[tx.txid].bestDescendant = null; |     mempool[tx.txid].bestDescendant = null; | ||||||
|     mempool[tx.txid].cpfpChecked = true; |     mempool[tx.txid].cpfpChecked = true; | ||||||
|     mempool[tx.txid].cpfpDirty = true; |     mempool[tx.txid].cpfpDirty = true; | ||||||
| @ -70,88 +216,12 @@ export function calculateCpfp(tx: MempoolTransactionExtended, mempool: { [txid: | |||||||
|     descendants: tx.descendants || [], |     descendants: tx.descendants || [], | ||||||
|     effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, |     effectiveFeePerVsize: tx.effectiveFeePerVsize || tx.adjustedFeePerVsize || tx.feePerVsize, | ||||||
|     sigops: tx.sigops, |     sigops: tx.sigops, | ||||||
|  |     fee: tx.fee, | ||||||
|     adjustedVsize: tx.adjustedVsize, |     adjustedVsize: tx.adjustedVsize, | ||||||
|     acceleration: tx.acceleration |     acceleration: tx.acceleration | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function mempoolToGraphTx(tx: MempoolTransactionExtended): GraphTx { |  | ||||||
|   return { |  | ||||||
|     ...tx, |  | ||||||
|     depends: tx.vin.map(v => v.txid), |  | ||||||
|     spentby: tx.vout.map((v, i) => memPool.getFromSpendMap(tx.txid, i)).map(tx => tx?.txid).filter(txid => txid != null) as string[], |  | ||||||
|     ancestorMap: new Map(), |  | ||||||
|     fees: { |  | ||||||
|       base: tx.fee, |  | ||||||
|       ancestor: tx.fee, |  | ||||||
|     }, |  | ||||||
|     ancestorcount: 1, |  | ||||||
|     ancestorsize: tx.adjustedVsize, |  | ||||||
|     ancestorRate: 0, |  | ||||||
|     individualRate: 0, |  | ||||||
|     score: 0, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives |  | ||||||
|  */ |  | ||||||
| function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>): Map<string, GraphTx> { |  | ||||||
|   const relatives: Map<string, GraphTx> = new Map(); |  | ||||||
|   const stack: GraphTx[] = Array.from(ancestors.values()); |  | ||||||
|   while (stack.length > 0) { |  | ||||||
|     if (relatives.size > MAX_GRAPH_SIZE) { |  | ||||||
|       return relatives; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const nextTx = stack.pop(); |  | ||||||
|     if (!nextTx) { |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     relatives.set(nextTx.txid, nextTx); |  | ||||||
| 
 |  | ||||||
|     for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) { |  | ||||||
|       if (relatives.has(relativeTxid)) { |  | ||||||
|         // already processed this tx
 |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       let mempoolTx = ancestors.get(relativeTxid); |  | ||||||
|       if (!mempoolTx && mempool[relativeTxid]) { |  | ||||||
|         mempoolTx = mempoolToGraphTx(mempool[relativeTxid]); |  | ||||||
|       } |  | ||||||
|       if (mempoolTx) { |  | ||||||
|         stack.push(mempoolTx); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return relatives; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
|  /** |  | ||||||
|    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph |  | ||||||
|    * by running setAncestors on each leaf, and caching intermediate results. |  | ||||||
|    * then initializes ancestor data for each transaction |  | ||||||
|    *  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
|  function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> { |  | ||||||
|   const visited: Map<string, Map<string, GraphTx>> = new Map(); |  | ||||||
|   const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); |  | ||||||
|   for (const leaf of leaves) { |  | ||||||
|     setAncestors(leaf, mempoolTxs, visited); |  | ||||||
|   } |  | ||||||
|   mempoolTxs.forEach(entry => { |  | ||||||
|     entry.ancestorMap?.forEach(ancestor => { |  | ||||||
|       entry.ancestorcount++; |  | ||||||
|       entry.ancestorsize += ancestor.adjustedVsize; |  | ||||||
|       entry.fees.ancestor += ancestor.fees.base; |  | ||||||
|     }); |  | ||||||
|     setAncestorScores(entry); |  | ||||||
|   }); |  | ||||||
|   return mempoolTxs; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|    * Given a root transaction and a list of in-mempool ancestors, |    * Given a root transaction and a list of in-mempool ancestors, | ||||||
|    * Calculate the CPFP cluster |    * Calculate the CPFP cluster | ||||||
| @ -172,10 +242,10 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|   let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); |   let sortedRelatives = Array.from(graph.values()).sort(mempoolComparator); | ||||||
| 
 | 
 | ||||||
|   // Iterate until we reach a cluster that includes our target tx
 |   // Iterate until we reach a cluster that includes our target tx
 | ||||||
|   let maxIterations = MAX_GRAPH_SIZE; |   let maxIterations = MAX_CLUSTER_ITERATIONS; | ||||||
|   let best = sortedRelatives.shift(); |   let best = sortedRelatives.shift(); | ||||||
|   let bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []); |   let bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []); | ||||||
|   while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestorMap.has(tx.txid)) && maxIterations > 0) { |   while (sortedRelatives.length && best && (best.txid !== tx.txid && !best.ancestors.has(tx.txid)) && maxIterations > 0) { | ||||||
|     maxIterations--; |     maxIterations--; | ||||||
|     if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { |     if ((best && best.txid === tx.txid) || (bestCluster && bestCluster.has(tx.txid))) { | ||||||
|       break; |       break; | ||||||
| @ -190,7 +260,7 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|       // Grab the next highest scoring entry
 |       // Grab the next highest scoring entry
 | ||||||
|       best = sortedRelatives.shift(); |       best = sortedRelatives.shift(); | ||||||
|       if (best) { |       if (best) { | ||||||
|         bestCluster = new Map<string, GraphTx>(best?.ancestorMap?.entries() || []); |         bestCluster = new Map<string, GraphTx>(best?.ancestors?.entries() || []); | ||||||
|         bestCluster.set(best?.txid, best); |         bestCluster.set(best?.txid, best); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -199,88 +269,4 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st | |||||||
|   bestCluster.set(tx.txid, tx); |   bestCluster.set(tx.txid, tx); | ||||||
| 
 | 
 | ||||||
|   return bestCluster; |   return bestCluster; | ||||||
| } |  | ||||||
| 
 |  | ||||||
|  /** |  | ||||||
|    * Remove a cluster of transactions from an in-mempool dependency graph |  | ||||||
|    * and update the survivors' scores and ancestors |  | ||||||
|    *  |  | ||||||
|    * @param cluster  |  | ||||||
|    * @param ancestors  |  | ||||||
|    */ |  | ||||||
|  function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void { |  | ||||||
|   // remove
 |  | ||||||
|   cluster.forEach(tx => { |  | ||||||
|     all.delete(tx.txid); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   // update survivors
 |  | ||||||
|   all.forEach(tx => { |  | ||||||
|     cluster.forEach(remove => { |  | ||||||
|       if (tx.ancestorMap?.has(remove.txid)) { |  | ||||||
|         // remove as dependency
 |  | ||||||
|         tx.ancestorMap.delete(remove.txid); |  | ||||||
|         tx.depends = tx.depends.filter(parent => parent !== remove.txid); |  | ||||||
|         // update ancestor sizes and fees
 |  | ||||||
|         tx.ancestorsize -= remove.adjustedVsize; |  | ||||||
|         tx.fees.ancestor -= remove.fees.base; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     // recalculate fee rates
 |  | ||||||
|     setAncestorScores(tx); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|    * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors |  | ||||||
|    * for each transaction. |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    * @param all  |  | ||||||
|    */ |  | ||||||
| function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> { |  | ||||||
|   // sanity check for infinite recursion / too many ancestors (should never happen)
 |  | ||||||
|   if (depth > MAX_GRAPH_SIZE) { |  | ||||||
|     return tx.ancestorMap; |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   // initialize the ancestor map for this tx
 |  | ||||||
|   tx.ancestorMap = new Map<string, GraphTx>(); |  | ||||||
|   tx.depends.forEach(parentId => { |  | ||||||
|     const parent = all.get(parentId); |  | ||||||
|     if (parent) { |  | ||||||
|       // add the parent
 |  | ||||||
|       tx.ancestorMap?.set(parentId, parent); |  | ||||||
|       // check for a cached copy of this parent's ancestors
 |  | ||||||
|       let ancestors = visited.get(parent.txid); |  | ||||||
|       if (!ancestors) { |  | ||||||
|         // recursively fetch the parent's ancestors
 |  | ||||||
|         ancestors = setAncestors(parent, all, visited, depth + 1); |  | ||||||
|       } |  | ||||||
|       // and add to this tx's map
 |  | ||||||
|       ancestors.forEach((ancestor, ancestorId) => { |  | ||||||
|         tx.ancestorMap?.set(ancestorId, ancestor); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   visited.set(tx.txid, tx.ancestorMap); |  | ||||||
| 
 |  | ||||||
|   return tx.ancestorMap; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|    * Take a mempool transaction, and set the fee rates and ancestor score |  | ||||||
|    *  |  | ||||||
|    * @param tx  |  | ||||||
|    */ |  | ||||||
| function setAncestorScores(tx: GraphTx): GraphTx { |  | ||||||
|   tx.individualRate = (tx.fees.base * 100_000_000) / tx.adjustedVsize; |  | ||||||
|   tx.ancestorRate = (tx.fees.ancestor * 100_000_000) / tx.ancestorsize; |  | ||||||
|   tx.score = Math.min(tx.individualRate, tx.ancestorRate); |  | ||||||
|   return tx; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Sort by descending score
 |  | ||||||
| function mempoolComparator(a: GraphTx, b: GraphTx): number { |  | ||||||
|   return b.score - a.score; |  | ||||||
| } | } | ||||||
| @ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; | |||||||
| import { RowDataPacket } from 'mysql2'; | import { RowDataPacket } from 'mysql2'; | ||||||
| 
 | 
 | ||||||
| class DatabaseMigration { | class DatabaseMigration { | ||||||
|   private static currentVersion = 76; |   private static currentVersion = 82; | ||||||
|   private queryTimeout = 3600_000; |   private queryTimeout = 3600_000; | ||||||
|   private statisticsAddedIndexed = false; |   private statisticsAddedIndexed = false; | ||||||
|   private uniqueLogs: string[] = []; |   private uniqueLogs: string[] = []; | ||||||
| @ -653,9 +653,11 @@ class DatabaseMigration { | |||||||
|       await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"'); |       await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"'); | ||||||
|       await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"'); |       await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"'); | ||||||
| 
 | 
 | ||||||
|       await this.$executeQuery('TRUNCATE hashrates'); |       if (isBitcoin === true) { | ||||||
|       await this.$executeQuery('TRUNCATE difficulty_adjustments'); |         await this.$executeQuery('TRUNCATE hashrates'); | ||||||
|       await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); |         await this.$executeQuery('TRUNCATE difficulty_adjustments'); | ||||||
|  |         await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       await this.updateToSchemaVersion(75); |       await this.updateToSchemaVersion(75); | ||||||
|     } |     } | ||||||
| @ -664,6 +666,45 @@ class DatabaseMigration { | |||||||
|       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"'); |       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"'); | ||||||
|       await this.updateToSchemaVersion(76); |       await this.updateToSchemaVersion(76); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 77 && config.MEMPOOL.NETWORK === 'mainnet') { | ||||||
|  |       await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL'); | ||||||
|  |       await this.updateToSchemaVersion(77); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 78) { | ||||||
|  |       await this.$executeQuery('ALTER TABLE `prices` CHANGE `time` `time` datetime NOT NULL'); | ||||||
|  |       await this.updateToSchemaVersion(78); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 79 && config.MEMPOOL.NETWORK === 'mainnet') { | ||||||
|  |       // Clear bad data
 | ||||||
|  |       await this.$executeQuery(`TRUNCATE accelerations`); | ||||||
|  |       this.uniqueLog(logger.notice, `'accelerations' table has been truncated`); | ||||||
|  |       await this.$executeQuery(` | ||||||
|  |         UPDATE state | ||||||
|  |         SET number = 0 | ||||||
|  |         WHERE name = 'last_acceleration_block' | ||||||
|  |       `);
 | ||||||
|  |       await this.updateToSchemaVersion(79); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 80) { | ||||||
|  |       await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL'); | ||||||
|  |       await this.updateToSchemaVersion(80); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 81 && isBitcoin === true) { | ||||||
|  |       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)'); | ||||||
|  |       await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); | ||||||
|  |       await this.updateToSchemaVersion(81); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') { | ||||||
|  |       await this.$fixBadV1AuditBlocks(); | ||||||
|  |       await this.updateToSchemaVersion(82); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
| @ -1278,6 +1319,28 @@ class DatabaseMigration { | |||||||
|       logger.warn(`Failed to migrate cpfp transaction data`); |       logger.warn(`Failed to migrate cpfp transaction data`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private async $fixBadV1AuditBlocks(): Promise<void> { | ||||||
|  |     const badBlocks = [ | ||||||
|  |       '000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc', | ||||||
|  |       '000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960', | ||||||
|  |       '000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7', | ||||||
|  |       '00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286', | ||||||
|  |       '0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for (const hash of badBlocks) { | ||||||
|  |       try { | ||||||
|  |         await this.$executeQuery(` | ||||||
|  |           UPDATE blocks_audits | ||||||
|  |           SET prioritized_txs = '[]' | ||||||
|  |           WHERE hash = '${hash}' | ||||||
|  |         `, true);
 | ||||||
|  |       } catch (e) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new DatabaseMigration(); | export default new DatabaseMigration(); | ||||||
|  | |||||||
| @ -54,9 +54,11 @@ class ChannelsRoutes { | |||||||
| 
 | 
 | ||||||
|       if (index < -1) { |       if (index < -1) { | ||||||
|         res.status(400).send('Invalid index'); |         res.status(400).send('Invalid index'); | ||||||
|  |         return; | ||||||
|       } |       } | ||||||
|       if (['open', 'active', 'closed'].includes(status) === false) { |       if (['open', 'active', 'closed'].includes(status) === false) { | ||||||
|         res.status(400).send('Invalid status'); |         res.status(400).send('Invalid status'); | ||||||
|  |         return; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status); |       const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status); | ||||||
|  | |||||||
| @ -666,7 +666,9 @@ class NodesApi { | |||||||
|         node.last_update = null; |         node.last_update = null; | ||||||
|       } |       } | ||||||
|    |    | ||||||
|       const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; |       const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))]; | ||||||
|  |       const formattedSockets = (uniqueAddr.join(',')) ?? ''; | ||||||
|  | 
 | ||||||
|       const query = `INSERT INTO nodes(
 |       const query = `INSERT INTO nodes(
 | ||||||
|           public_key, |           public_key, | ||||||
|           first_seen, |           first_seen, | ||||||
| @ -695,13 +697,13 @@ class NodesApi { | |||||||
|         node.alias, |         node.alias, | ||||||
|         this.aliasToSearchText(node.alias), |         this.aliasToSearchText(node.alias), | ||||||
|         node.color, |         node.color, | ||||||
|         sockets, |         formattedSockets, | ||||||
|         JSON.stringify(node.features), |         JSON.stringify(node.features), | ||||||
|         node.last_update, |         node.last_update, | ||||||
|         node.alias, |         node.alias, | ||||||
|         this.aliasToSearchText(node.alias), |         this.aliasToSearchText(node.alias), | ||||||
|         node.color, |         node.color, | ||||||
|         sockets, |         formattedSockets, | ||||||
|         JSON.stringify(node.features), |         JSON.stringify(node.features), | ||||||
|       ]); |       ]); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @ -713,7 +715,9 @@ class NodesApi { | |||||||
|    * Update node sockets |    * Update node sockets | ||||||
|    */ |    */ | ||||||
|   public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> { |   public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> { | ||||||
|     const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? ''; |     const uniqueAddr = [...new Set(sockets.map(a => a.addr))]; | ||||||
|  | 
 | ||||||
|  |     const formattedSockets = (uniqueAddr.join(',')) ?? ''; | ||||||
|     try { |     try { | ||||||
|       await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]); |       await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  | |||||||
| @ -48,6 +48,14 @@ class NodesRoutes { | |||||||
|             '032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470', |             '032850492ee61a5f7006a2fda6925e4b4ec3782f2b6de2ff0e439ef5a38c3b2470', | ||||||
|             '022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421', |             '022c80bace98831c44c32fb69755f2b353434e0ee9e7fbda29507f7ef8abea1421', | ||||||
|             '02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680', |             '02c3559c833e6f99f9ca05fe503e0b4e7524dea9121344edfd3e811101e0c28680', | ||||||
|  |             '02b36a324fa2dd3af2a63ac65f241907882829bed5002b4e14171d25c219e0d470', | ||||||
|  |             '0231b6e8f21f9f6c057f6bf8a812f79e396ee16a66ece91939a1576ce9fb9e87a5', | ||||||
|  |             '034b6aac206bffcbd651b7ead1ab8a0991c945dfafe19ff27dcdeadc6843ebd15c', | ||||||
|  |             '039c065f7e344acd969ebdd4a94550915b6f24e8782ae2be540bb96c8a4fcfb86b', | ||||||
|  |             '03d9f9f4803fc75920f14dd13d83fbecc53229a65d4ee4cd2d86fdf211f7337576', | ||||||
|  |             '0357fe48c4dece744f70865eda66e396aab5d05e09e1145cd3b7da83f11446d4cf', | ||||||
|  |             '02bca4d642eda631f2c8659758e2a2868e518b93503f2bfcd767749c6530a10679', | ||||||
|  |             '03f32c99c0bb9f62dae53671d1d300565773455248f34134cc02779b881561174e', | ||||||
|             '032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', |             '032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', | ||||||
|             '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', |             '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', | ||||||
|             '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', |             '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', | ||||||
| @ -60,6 +68,14 @@ class NodesRoutes { | |||||||
|             '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', |             '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', | ||||||
|             '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', |             '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', | ||||||
|             '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584', |             '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584', | ||||||
|  |             '038eb09bed4532ff36d12acc1279f55cbe8d95212d19f809e057bb50de00051fba', | ||||||
|  |             '027b7c0278366a0268e8bd0072b14539f6cb455a7bd588ae22d888bed541f65311', | ||||||
|  |             '02f4dd78f6eda8838029b2cdbaaea6e875e2fa373cd348ee41a7c1bb177d3fca66', | ||||||
|  |             '036b3fb692da214a3edaac5b67903b958f5ccd8712e09aa61b67ea7acfd94b40c2', | ||||||
|  |             '023bc8915d308e0b65f8de6867f95960141372436fce3edad5cec3f364d6ac948f', | ||||||
|  |             '0341690503ef21d0e203dddd9e62646380d0dfc32c499e055e7f698b9064d1c736', | ||||||
|  |             '0355d573805c018a37a5b2288378d70e9b5b438f7394abd6f467cb9b47c90eeb93', | ||||||
|  |             '0361aa68deb561a8b47b41165848edcccb98a1b56a5ea922d9d5b30a09bb7282ea', | ||||||
|             '0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c', |             '0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c', | ||||||
|             '029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5', |             '029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5', | ||||||
|             '02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075', |             '02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075', | ||||||
| @ -76,6 +92,14 @@ class NodesRoutes { | |||||||
|             '0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc', |             '0243348cb3741cfe2d8485fa8375c29c7bc7cbb67577c363cb6987a5e5fd0052cc', | ||||||
|             '02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9', |             '02cb73e631af44bee600d80f8488a9194c9dc5c7590e575c421a070d1be05bc8e9', | ||||||
|             '0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4', |             '0306f55ee631aa1e2cd4d9b2bfcbc14404faec5c541cef8b2e6f779061029d09c4', | ||||||
|  |             '030bbbd8495561a894e301fe6ba5b22f8941fc661cc0e673e0206158231d8ac130', | ||||||
|  |             '03ee1f08e516ed083475f39c6cae4fa1eec686d004d2f105218269e27d7f2da5a4', | ||||||
|  |             '028c378b998f476ed22d6815c170dd2a3388a43fdf791a7cff70b9997349b8447a', | ||||||
|  |             '036f19f044d19cb1b04f14d91b6e7e5443ce337217a8c14d43861f3e86dd07bd7f', | ||||||
|  |             '03058d61869e8b88436493648b2e3e530627edf5a0b253c285cd565c1477a5c237', | ||||||
|  |             '0279dfedc87b47a941f1797f2c422c03aa3108914ea6b519d76537d60860535a9a', | ||||||
|  |             '0353486b8016761e58ec8aee7305ee58d5dc66b55ef5bd8cbaf49508f66d52d62e', | ||||||
|  |             '03df5db8eccfabcae47ff15553cfdecb2d3f56979f43a0c3578f28d056b5e35104', | ||||||
|             '03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', |             '03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', | ||||||
|             '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', |             '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', | ||||||
|             '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', |             '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', | ||||||
| @ -88,6 +112,14 @@ class NodesRoutes { | |||||||
|             '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', |             '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', | ||||||
|             '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', |             '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', | ||||||
|             '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7', |             '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7', | ||||||
|  |             '0326cf9a4ca67a5b9cdffae57293dbd6f7c5113b93010dc6f6fe4af3afde1a1739', | ||||||
|  |             '034867e16f62cebb8c2c2c22b91117c173bbece9c8a1e5bd001374a3699551cd8f', | ||||||
|  |             '038dfb1f1b637a8c27e342ffc6f9feca20e0b47be3244e09ae78df4998e2ae83b9', | ||||||
|  |             '03cb1cea3394d973355c11bc61c2f689f9d3e1c3db60d205f27770f5ad83200f77', | ||||||
|  |             '03535447b592cbdb153189b3e06a455452b1011380cb3e6511a31090c15d8efc9f', | ||||||
|  |             '028e90e9984d262ebfa3c23fb3f335a2ae061a0bdedee03f45f72b438d9e7d2ce3', | ||||||
|  |             '03ee0176289dc4a6111fa5ef22eed5273758c420fbe58cc1d2d76def75dd7e640c', | ||||||
|  |             '0370b2cd9f0eaf436d5c25c93fb39210d8cc06b31f688fc2f54418aabe394aed79', | ||||||
|             '02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070', |             '02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070', | ||||||
|             '02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45', |             '02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45', | ||||||
|             '038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097', |             '038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097', | ||||||
| @ -104,6 +136,14 @@ class NodesRoutes { | |||||||
|             '03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc', |             '03229ab4b7f692753e094b93df90530150680f86b535b5183b0cffd75b3df583fc', | ||||||
|             '03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e', |             '03a696eb7acde991c1be97a58a9daef416659539ae462b897f5e9ae361f990228e', | ||||||
|             '0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055', |             '0248bf26cf3a63ab8870f34dc0ec9e6c8c6288cdba96ba3f026f34ec0f13ac4055', | ||||||
|  |             '021b28ecdd782fd909705d6be354db268977b1a2ac5a5275186fc19e08bb8fca93', | ||||||
|  |             '031bec1fbd8eb7fe94d2bda108c9c3cc8c22ecfc1c3a5c11d36f5881b01b4a81a6', | ||||||
|  |             '03879c4f827a3188574d5757e002f574265a966d70aea942169785b31369b067d5', | ||||||
|  |             '0228d4b5a4fd73a03967b76f8b8cb37b9d0b6e7039126a9397bb732c15bed78e9b', | ||||||
|  |             '03f58dbb629f4427f5a1dbc02e6a7ec79345fdf13a0e4163d4f3b7aea2539cf095', | ||||||
|  |             '021cdcb8123aa670cdfc9f43909dbb297363c093883409e9e7fc82e7267f7c72bd', | ||||||
|  |             '02f2aa2c2b7b432a70dc4d0b04afa19d48715ed3b90594d49c1c8744f2e9ebb030', | ||||||
|  |             '03709a02fb3ab4857689a8ea0bd489a6ab6f56f8a397be578bc6d5ad22efbe3756', | ||||||
|             '03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', |             '03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', | ||||||
|             '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', |             '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', | ||||||
|             '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', |             '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', | ||||||
| @ -116,6 +156,14 @@ class NodesRoutes { | |||||||
|             '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', |             '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', | ||||||
|             '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', |             '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', | ||||||
|             '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43', |             '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43', | ||||||
|  |             '03b4dda7878d3b7b71ecd6d4738322c7f9a9c1fb583374d2724f4ccc4947f37570', | ||||||
|  |             '0279a35f05b5acf159429549e56fd426685c4fec191431c58738968bbc77a39f25', | ||||||
|  |             '03cb102d796ddcf08610cd03fae8b7a1df69ff48e9e8a152af315f9edf71762eb8', | ||||||
|  |             '036b89526f4d5ac4c317f4fd23cb9f8e4ad844498bc7950a41114d060101d995d4', | ||||||
|  |             '0313eade145959d7036db009fd5b0bf1947a739c7c3c790b491ec9161b94e6ad1e', | ||||||
|  |             '02b670ca4c4bb2c5ea89c3b691da98a194cfc48fcd5c072df02a20290bddd60610', | ||||||
|  |             '02a9196d5e08598211397a83cf013a5962b84bd61198abfdd204dff987e54f7a0d', | ||||||
|  |             '036d015cd2f486fb38348182980b7e596e6c9733873102ea126fed7b4152be03b8', | ||||||
|             '02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06', |             '02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06', | ||||||
|             '0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e', |             '0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e', | ||||||
|             '03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7', |             '03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7', | ||||||
|  | |||||||
| @ -1,11 +1,13 @@ | |||||||
| import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt'; | import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces'; | import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces'; | ||||||
| import { Common, OnlineFeeStatsCalculator } from './common'; | import { Common, OnlineFeeStatsCalculator } from './common'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import { Worker } from 'worker_threads'; | import { Worker } from 'worker_threads'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import mempool from './mempool'; | import mempool from './mempool'; | ||||||
|  | import { Acceleration } from './services/acceleration'; | ||||||
|  | import PoolsRepository from '../repositories/PoolsRepository'; | ||||||
| 
 | 
 | ||||||
| const MAX_UINT32 = Math.pow(2, 32) - 1; | const MAX_UINT32 = Math.pow(2, 32) - 1; | ||||||
| 
 | 
 | ||||||
| @ -14,12 +16,14 @@ class MempoolBlocks { | |||||||
|   private mempoolBlockDeltas: MempoolBlockDelta[] = []; |   private mempoolBlockDeltas: MempoolBlockDelta[] = []; | ||||||
|   private txSelectionWorker: Worker | null = null; |   private txSelectionWorker: Worker | null = null; | ||||||
|   private rustInitialized: boolean = false; |   private rustInitialized: boolean = false; | ||||||
|   private rustGbtGenerator: GbtGenerator = new GbtGenerator(); |   private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); | ||||||
| 
 | 
 | ||||||
|   private nextUid: number = 1; |   private nextUid: number = 1; | ||||||
|   private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
 |   private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
 | ||||||
|   private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
 |   private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
 | ||||||
| 
 | 
 | ||||||
|  |   private pools: { [id: number]: PoolTag } = {}; | ||||||
|  | 
 | ||||||
|   public getMempoolBlocks(): MempoolBlock[] { |   public getMempoolBlocks(): MempoolBlock[] { | ||||||
|     return this.mempoolBlocks.map((block) => { |     return this.mempoolBlocks.map((block) => { | ||||||
|       return { |       return { | ||||||
| @ -41,6 +45,18 @@ class MempoolBlocks { | |||||||
|     return this.mempoolBlockDeltas; |     return this.mempoolBlockDeltas; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async updatePools$(): Promise<void> { | ||||||
|  |     if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { | ||||||
|  |       this.pools = {}; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const allPools = await PoolsRepository.$getPools(); | ||||||
|  |     this.pools = {}; | ||||||
|  |     for (const pool of allPools) { | ||||||
|  |       this.pools[pool.uniqueId] = pool; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] { |   private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] { | ||||||
|     const mempoolBlockDeltas: MempoolBlockDelta[] = []; |     const mempoolBlockDeltas: MempoolBlockDelta[] = []; | ||||||
|     for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { |     for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { | ||||||
| @ -214,7 +230,7 @@ class MempoolBlocks { | |||||||
| 
 | 
 | ||||||
|   private resetRustGbt(): void { |   private resetRustGbt(): void { | ||||||
|     this.rustInitialized = false; |     this.rustInitialized = false; | ||||||
|     this.rustGbtGenerator = new GbtGenerator(); |     this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> { |   public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> { | ||||||
| @ -246,7 +262,7 @@ class MempoolBlocks { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // run the block construction algorithm in a separate thread, and wait for a result
 |     // run the block construction algorithm in a separate thread, and wait for a result
 | ||||||
|     const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(); |     const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT); | ||||||
|     try { |     try { | ||||||
|       const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids( |       const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids( | ||||||
|         await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid), |         await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid), | ||||||
| @ -333,10 +349,13 @@ class MempoolBlocks { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] { |   private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations: { [txid: string]: Acceleration }, accelerationPool, saveResults): MempoolBlockWithTransactions[] { | ||||||
|     for (const txid of Object.keys(candidates?.txs ?? mempool)) { |     for (const txid of Object.keys(candidates?.txs ?? mempool)) { | ||||||
|       if (txid in mempool) { |       if (txid in mempool) { | ||||||
|         mempool[txid].cpfpDirty = false; |         mempool[txid].cpfpDirty = false; | ||||||
|  |         mempool[txid].ancestors = []; | ||||||
|  |         mempool[txid].descendants = []; | ||||||
|  |         mempool[txid].bestDescendant = null; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     for (const [txid, rate] of rates) { |     for (const [txid, rate] of rates) { | ||||||
| @ -396,7 +415,7 @@ class MempoolBlocks { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const isAccelerated : { [txid: string]: boolean } = {}; |     const isAcceleratedBy : { [txid: string]: number[] | false } = {}; | ||||||
| 
 | 
 | ||||||
|     const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; |     const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; | ||||||
|     // update this thread's mempool with the results
 |     // update this thread's mempool with the results
 | ||||||
| @ -427,17 +446,23 @@ class MempoolBlocks { | |||||||
|           }; |           }; | ||||||
| 
 | 
 | ||||||
|           const acceleration = accelerations[txid]; |           const acceleration = accelerations[txid]; | ||||||
|           if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { |           if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { | ||||||
|             if (!mempoolTx.acceleration) { |             if (!mempoolTx.acceleration) { | ||||||
|               mempoolTx.cpfpDirty = true; |               mempoolTx.cpfpDirty = true; | ||||||
|             } |             } | ||||||
|             mempoolTx.acceleration = true; |             mempoolTx.acceleration = true; | ||||||
|  |             mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; | ||||||
|  |             mempoolTx.acceleratedAt = acceleration?.added; | ||||||
|  |             mempoolTx.feeDelta = acceleration?.feeDelta; | ||||||
|             for (const ancestor of mempoolTx.ancestors || []) { |             for (const ancestor of mempoolTx.ancestors || []) { | ||||||
|               if (!mempool[ancestor.txid].acceleration) { |               if (!mempool[ancestor.txid].acceleration) { | ||||||
|                 mempool[ancestor.txid].cpfpDirty = true; |                 mempool[ancestor.txid].cpfpDirty = true; | ||||||
|               } |               } | ||||||
|               mempool[ancestor.txid].acceleration = true; |               mempool[ancestor.txid].acceleration = true; | ||||||
|               isAccelerated[ancestor.txid] = true; |               mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; | ||||||
|  |               mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; | ||||||
|  |               mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; | ||||||
|  |               isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; | ||||||
|             } |             } | ||||||
|           } else { |           } else { | ||||||
|             if (mempoolTx.acceleration) { |             if (mempoolTx.acceleration) { | ||||||
| @ -475,7 +500,7 @@ class MempoolBlocks { | |||||||
|       const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); |       const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); | ||||||
|       this.mempoolBlocks = mempoolBlocks; |       this.mempoolBlocks = mempoolBlocks; | ||||||
|       this.mempoolBlockDeltas = deltas; |       this.mempoolBlockDeltas = deltas; | ||||||
| 
 |       this.updateAccelerationPositions(mempool, accelerations, mempoolBlocks); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return mempoolBlocks; |     return mempoolBlocks; | ||||||
| @ -622,6 +647,124 @@ class MempoolBlocks { | |||||||
|       tx.acc ? 1 : 0, |       tx.acc ? 1 : 0, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // estimates and saves positions of accelerations in mining partner mempools
 | ||||||
|  |   private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void { | ||||||
|  |     const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; | ||||||
|  |     // keep track of simulated mempool blocks for each active pool
 | ||||||
|  |     const pools: { | ||||||
|  |       [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; | ||||||
|  |     } = {}; | ||||||
|  |     // prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
 | ||||||
|  |     const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => { | ||||||
|  |       let vsize = mempoolCache[acc.txid].vsize; | ||||||
|  |       for (const ancestor of mempoolCache[acc.txid].ancestors || []) { | ||||||
|  |         vsize += (ancestor.weight / 4); | ||||||
|  |       } | ||||||
|  |       return { | ||||||
|  |         acceleration: acc, | ||||||
|  |         rate: mempoolCache[acc.txid].effectiveFeePerVsize, | ||||||
|  |         vsize | ||||||
|  |       }; | ||||||
|  |     }).sort((a, b) => a.rate - b.rate); | ||||||
|  |     // initialize the pool tracker
 | ||||||
|  |     for (const { acceleration }  of accQueue) { | ||||||
|  |       accelerationPositions[acceleration.txid] = []; | ||||||
|  |       for (const pool of acceleration.pools) { | ||||||
|  |         if (!pools[pool]) { | ||||||
|  |           pools[pool] = { | ||||||
|  |             name: this.pools[pool]?.name || 'unknown', | ||||||
|  |             block: 0, | ||||||
|  |             vsize: 0, | ||||||
|  |             accelerations: [], | ||||||
|  |             complete: false, | ||||||
|  |           }; | ||||||
|  |         } | ||||||
|  |         pools[pool].accelerations.push(acceleration.txid); | ||||||
|  |       } | ||||||
|  |       for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) { | ||||||
|  |         accelerationPositions[ancestor.txid] = []; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const pool of Object.keys(pools)) { | ||||||
|  |       // if any pools accepted *every* acceleration, we can just use the GBT result positions directly
 | ||||||
|  |       if (pools[pool].accelerations.length === Object.keys(accelerations).length) { | ||||||
|  |         pools[pool].complete = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let block = 0; | ||||||
|  |     let index = 0; | ||||||
|  |     let next = accQueue.pop(); | ||||||
|  |     // build simulated blocks for each pool by taking the best option from
 | ||||||
|  |     // either the mempool or the list of accelerations.
 | ||||||
|  |     while (next && block < mempoolBlocks.length) { | ||||||
|  |       while (next && index < mempoolBlocks[block].transactions.length) { | ||||||
|  |         const nextTx = mempoolBlocks[block].transactions[index]; | ||||||
|  |         if (next.rate >= (nextTx.rate || (nextTx.fee / nextTx.vsize))) { | ||||||
|  |           for (const pool of next.acceleration.pools) { | ||||||
|  |             if (pools[pool].vsize + next.vsize <= 999_000) { | ||||||
|  |               pools[pool].vsize += next.vsize; | ||||||
|  |             } else { | ||||||
|  |               pools[pool].block++; | ||||||
|  |               pools[pool].vsize = next.vsize; | ||||||
|  |             } | ||||||
|  |             // insert the acceleration into matching pool's blocks
 | ||||||
|  |             if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) { | ||||||
|  |               accelerationPositions[next.acceleration.txid].push({ | ||||||
|  |                 ...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number }, | ||||||
|  |                 poolId: pool, | ||||||
|  |                 pool: pools[pool].name | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               accelerationPositions[next.acceleration.txid].push({ | ||||||
|  |                 poolId: pool, | ||||||
|  |                 pool: pools[pool].name, | ||||||
|  |                 block: pools[pool].block, | ||||||
|  |                 vsize: pools[pool].vsize - (next.vsize / 2), | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |             // and any accelerated ancestors
 | ||||||
|  |             for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) { | ||||||
|  |               if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) { | ||||||
|  |                 accelerationPositions[ancestor.txid].push({ | ||||||
|  |                   ...mempoolCache[ancestor.txid].position as { block: number, vsize: number }, | ||||||
|  |                   poolId: pool, | ||||||
|  |                   pool: pools[pool].name, | ||||||
|  |                 }); | ||||||
|  |               } else { | ||||||
|  |                 accelerationPositions[ancestor.txid].push({ | ||||||
|  |                   poolId: pool, | ||||||
|  |                   pool: pools[pool].name, | ||||||
|  |                   block: pools[pool].block, | ||||||
|  |                   vsize: pools[pool].vsize - (next.vsize / 2), | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           next = accQueue.pop(); | ||||||
|  |         } else { | ||||||
|  |           // skip accelerated transactions and their CPFP ancestors
 | ||||||
|  |           if (accelerationPositions[nextTx.txid] == null) { | ||||||
|  |             // insert into all pools' blocks
 | ||||||
|  |             for (const pool of Object.keys(pools)) { | ||||||
|  |               if (pools[pool].vsize + nextTx.vsize <= 999_000) { | ||||||
|  |                 pools[pool].vsize += nextTx.vsize; | ||||||
|  |               } else { | ||||||
|  |                 pools[pool].block++; | ||||||
|  |                 pools[pool].vsize = nextTx.vsize; | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           index++; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       block++; | ||||||
|  |       index = 0; | ||||||
|  |     } | ||||||
|  |     mempool.setAccelerationPositions(accelerationPositions); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new MempoolBlocks(); | export default new MempoolBlocks(); | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ class Mempool { | |||||||
|     deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined; |     deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined; | ||||||
| 
 | 
 | ||||||
|   private accelerations: { [txId: string]: Acceleration } = {}; |   private accelerations: { [txId: string]: Acceleration } = {}; | ||||||
|  |   private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; | ||||||
| 
 | 
 | ||||||
|   private txPerSecondArray: number[] = []; |   private txPerSecondArray: number[] = []; | ||||||
|   private txPerSecond: number = 0; |   private txPerSecond: number = 0; | ||||||
| @ -395,15 +396,15 @@ class Mempool { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public $updateAccelerations(newAccelerations: Acceleration[]): string[] { |   public $updateAccelerations(newAccelerations: Acceleration[]): string[] { | ||||||
|     if (!config.MEMPOOL_SERVICES.ACCELERATIONS) { |  | ||||||
|       return []; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     try { |     try { | ||||||
|       const changed: string[] = []; |       const changed: string[] = []; | ||||||
| 
 | 
 | ||||||
|       const newAccelerationMap: { [txid: string]: Acceleration } = {}; |       const newAccelerationMap: { [txid: string]: Acceleration } = {}; | ||||||
|       for (const acceleration of newAccelerations) { |       for (const acceleration of newAccelerations) { | ||||||
|  |         // skip transactions we don't know about
 | ||||||
|  |         if (!this.mempoolCache[acceleration.txid]) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|         newAccelerationMap[acceleration.txid] = acceleration; |         newAccelerationMap[acceleration.txid] = acceleration; | ||||||
|         if (this.accelerations[acceleration.txid] == null) { |         if (this.accelerations[acceleration.txid] == null) { | ||||||
|           // new acceleration
 |           // new acceleration
 | ||||||
| @ -510,6 +511,14 @@ class Mempool { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void { | ||||||
|  |     this.accelerationPositions = positions; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined { | ||||||
|  |     return this.accelerationPositions[txid]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private startTimer() { |   private startTimer() { | ||||||
|     const state: any = { |     const state: any = { | ||||||
|       start: Date.now(), |       start: Date.now(), | ||||||
|  | |||||||
							
								
								
									
										515
									
								
								backend/src/api/mini-miner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										515
									
								
								backend/src/api/mini-miner.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,515 @@ | |||||||
|  | import { Acceleration } from './acceleration/acceleration'; | ||||||
|  | import { MempoolTransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import logger from '../logger'; | ||||||
|  | 
 | ||||||
|  | const BLOCK_WEIGHT_UNITS = 4_000_000; | ||||||
|  | const BLOCK_SIGOPS = 80_000; | ||||||
|  | const MAX_RELATIVE_GRAPH_SIZE = 100; | ||||||
|  | 
 | ||||||
|  | export interface GraphTx { | ||||||
|  |   txid: string; | ||||||
|  |   vsize: number; | ||||||
|  |   weight: number; | ||||||
|  |   depends: string[]; | ||||||
|  |   spentby: string[]; | ||||||
|  | 
 | ||||||
|  |   ancestorcount: number; | ||||||
|  |   ancestorsize: number; | ||||||
|  |   fees: { // in sats
 | ||||||
|  |     base: number; | ||||||
|  |     ancestor: number; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   ancestors: Map<string, GraphTx>, | ||||||
|  |   ancestorRate: number; | ||||||
|  |   individualRate: number; | ||||||
|  |   score: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface TemplateTransaction { | ||||||
|  |   txid: string; | ||||||
|  |   order: number; | ||||||
|  |   weight: number; | ||||||
|  |   adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
 | ||||||
|  |   sigops: number; | ||||||
|  |   fee: number; | ||||||
|  |   feeDelta: number; | ||||||
|  |   ancestors: string[]; | ||||||
|  |   cluster: string[]; | ||||||
|  |   effectiveFeePerVsize: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface MinerTransaction extends TemplateTransaction { | ||||||
|  |   inputs: string[]; | ||||||
|  |   feePerVsize: number; | ||||||
|  |   relativesSet: boolean; | ||||||
|  |   ancestorMap: Map<string, MinerTransaction>; | ||||||
|  |   children: Set<MinerTransaction>; | ||||||
|  |   ancestorFee: number; | ||||||
|  |   ancestorVsize: number; | ||||||
|  |   ancestorSigops: number; | ||||||
|  |   score: number; | ||||||
|  |   used: boolean; | ||||||
|  |   modified: boolean; | ||||||
|  |   dependencyRate: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a raw transaction, and builds a graph of same-block relatives, | ||||||
|  |  * and returns as a GraphTx | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> { | ||||||
|  |   const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
 | ||||||
|  |   const spendMap = new Map<string, string>(); // map of outpoints to spending txids
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     blockTxs.set(tx.txid, tx); | ||||||
|  |     for (const vin of tx.vin) { | ||||||
|  |       spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const relatives: Map<string, GraphTx> = new Map(); | ||||||
|  |   const stack: string[] = [tx.txid]; | ||||||
|  | 
 | ||||||
|  |   // build set of same-block ancestors
 | ||||||
|  |   while (stack.length > 0) { | ||||||
|  |     const nextTxid = stack.pop(); | ||||||
|  |     const nextTx = nextTxid ? blockTxs.get(nextTxid) : null; | ||||||
|  |     if (!nextTx || relatives.has(nextTx.txid)) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const mempoolTx = convertToGraphTx(nextTx, spendMap); | ||||||
|  | 
 | ||||||
|  |     for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) { | ||||||
|  |       if (txid) { | ||||||
|  |         stack.push(txid); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     relatives.set(mempoolTx.txid, mempoolTx); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return relatives; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a raw transaction and converts it to GraphTx format | ||||||
|  |  * fee and ancestor data is initialized with dummy/null values | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function convertToGraphTx(tx: MempoolTransactionExtended, spendMap?: Map<string, MempoolTransactionExtended | string>): GraphTx { | ||||||
|  |   return { | ||||||
|  |     txid: tx.txid, | ||||||
|  |     vsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)), | ||||||
|  |     weight: tx.weight, | ||||||
|  |     fees: { | ||||||
|  |       base: tx.fee || 0, | ||||||
|  |       ancestor: tx.fee || 0, | ||||||
|  |     }, | ||||||
|  |     depends: (tx.vin.map(vin => vin.txid).filter(depend => depend) as string[]), | ||||||
|  |     spentby: spendMap ? (tx.vout.map((vout, index) => { const spend = spendMap.get(`${tx.txid}:${index}`); return (spend?.['txid'] || spend); }).filter(spent => spent) as string[]) : [], | ||||||
|  | 
 | ||||||
|  |     ancestorcount: 1, | ||||||
|  |     ancestorsize: Math.max(tx.sigops * 5, Math.ceil(tx.weight / 4)), | ||||||
|  |     ancestors: new Map<string, GraphTx>(), | ||||||
|  |     ancestorRate: 0, | ||||||
|  |     individualRate: 0, | ||||||
|  |     score: 0, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Takes a map of transaction ancestors, and expands it into a full graph of up to MAX_GRAPH_SIZE in-mempool relatives | ||||||
|  |  */ | ||||||
|  | export function expandRelativesGraph(mempool: { [txid: string]: MempoolTransactionExtended }, ancestors: Map<string, GraphTx>, spendMap: Map<string, MempoolTransactionExtended>): Map<string, GraphTx> { | ||||||
|  |   const relatives: Map<string, GraphTx> = new Map(); | ||||||
|  |   const stack: GraphTx[] = Array.from(ancestors.values()); | ||||||
|  |   while (stack.length > 0) { | ||||||
|  |     if (relatives.size > MAX_RELATIVE_GRAPH_SIZE) { | ||||||
|  |       return relatives; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const nextTx = stack.pop(); | ||||||
|  |     if (!nextTx) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     relatives.set(nextTx.txid, nextTx); | ||||||
|  | 
 | ||||||
|  |     for (const relativeTxid of [...nextTx.depends, ...nextTx.spentby]) { | ||||||
|  |       if (relatives.has(relativeTxid)) { | ||||||
|  |         // already processed this tx
 | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       let ancestorTx = ancestors.get(relativeTxid); | ||||||
|  |       if (!ancestorTx && relativeTxid in mempool) { | ||||||
|  |         const mempoolTx = mempool[relativeTxid]; | ||||||
|  |         ancestorTx = convertToGraphTx(mempoolTx, spendMap); | ||||||
|  |       } | ||||||
|  |       if (ancestorTx) { | ||||||
|  |         stack.push(ancestorTx); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return relatives; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors | ||||||
|  |  * for each transaction. | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  * @param all | ||||||
|  |  */ | ||||||
|  | function setAncestors(tx: GraphTx, all: Map<string, GraphTx>, visited: Map<string, Map<string, GraphTx>>, depth: number = 0): Map<string, GraphTx> { | ||||||
|  |   // sanity check for infinite recursion / too many ancestors (should never happen)
 | ||||||
|  |   if (depth > MAX_RELATIVE_GRAPH_SIZE) { | ||||||
|  |     logger.warn('cpfp dependency calculation failed: setAncestors reached depth of 100, unable to proceed'); | ||||||
|  |     return tx.ancestors; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // initialize the ancestor map for this tx
 | ||||||
|  |   tx.ancestors = new Map<string, GraphTx>(); | ||||||
|  |   tx.depends.forEach(parentId => { | ||||||
|  |     const parent = all.get(parentId); | ||||||
|  |     if (parent) { | ||||||
|  |       // add the parent
 | ||||||
|  |       tx.ancestors?.set(parentId, parent); | ||||||
|  |       // check for a cached copy of this parent's ancestors
 | ||||||
|  |       let ancestors = visited.get(parent.txid); | ||||||
|  |       if (!ancestors) { | ||||||
|  |         // recursively fetch the parent's ancestors
 | ||||||
|  |         ancestors = setAncestors(parent, all, visited, depth + 1); | ||||||
|  |       } | ||||||
|  |       // and add to this tx's map
 | ||||||
|  |       ancestors.forEach((ancestor, ancestorId) => { | ||||||
|  |         tx.ancestors?.set(ancestorId, ancestor); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   visited.set(tx.txid, tx.ancestors); | ||||||
|  | 
 | ||||||
|  |   return tx.ancestors; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |    * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph | ||||||
|  |    * by running setAncestors on each leaf, and caching intermediate results. | ||||||
|  |    * then initializes ancestor data for each transaction | ||||||
|  |    * | ||||||
|  |    * @param all | ||||||
|  |    */ | ||||||
|  | export function initializeRelatives(mempoolTxs: Map<string, GraphTx>): Map<string, GraphTx> { | ||||||
|  |   const visited: Map<string, Map<string, GraphTx>> = new Map(); | ||||||
|  |   const leaves: GraphTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0); | ||||||
|  |   for (const leaf of leaves) { | ||||||
|  |     setAncestors(leaf, mempoolTxs, visited); | ||||||
|  |   } | ||||||
|  |   mempoolTxs.forEach(entry => { | ||||||
|  |     entry.ancestors?.forEach(ancestor => { | ||||||
|  |       entry.ancestorcount++; | ||||||
|  |       entry.ancestorsize += ancestor.vsize; | ||||||
|  |       entry.fees.ancestor += ancestor.fees.base; | ||||||
|  |     }); | ||||||
|  |     setAncestorScores(entry); | ||||||
|  |   }); | ||||||
|  |   return mempoolTxs; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Remove a cluster of transactions from an in-mempool dependency graph | ||||||
|  |  * and update the survivors' scores and ancestors | ||||||
|  |  * | ||||||
|  |  * @param cluster | ||||||
|  |  * @param ancestors | ||||||
|  |  */ | ||||||
|  | export function removeAncestors(cluster: Map<string, GraphTx>, all: Map<string, GraphTx>): void { | ||||||
|  |   // remove
 | ||||||
|  |   cluster.forEach(tx => { | ||||||
|  |     all.delete(tx.txid); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // update survivors
 | ||||||
|  |   all.forEach(tx => { | ||||||
|  |     cluster.forEach(remove => { | ||||||
|  |       if (tx.ancestors?.has(remove.txid)) { | ||||||
|  |         // remove as dependency
 | ||||||
|  |         tx.ancestors.delete(remove.txid); | ||||||
|  |         tx.depends = tx.depends.filter(parent => parent !== remove.txid); | ||||||
|  |         // update ancestor sizes and fees
 | ||||||
|  |         tx.ancestorsize -= remove.vsize; | ||||||
|  |         tx.fees.ancestor -= remove.fees.base; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     // recalculate fee rates
 | ||||||
|  |     setAncestorScores(tx); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Take a mempool transaction, and set the fee rates and ancestor score | ||||||
|  |  * | ||||||
|  |  * @param tx | ||||||
|  |  */ | ||||||
|  | export function setAncestorScores(tx: GraphTx): void { | ||||||
|  |   tx.individualRate = tx.fees.base / tx.vsize; | ||||||
|  |   tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize; | ||||||
|  |   tx.score = Math.min(tx.individualRate, tx.ancestorRate); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Sort by descending score
 | ||||||
|  | export function mempoolComparator(a: GraphTx, b: GraphTx): number { | ||||||
|  |   return b.score - a.score; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | * Build a block using an approximation of the transaction selection algorithm from Bitcoin Core | ||||||
|  | * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
 | ||||||
|  | */ | ||||||
|  | export function makeBlockTemplate(candidates: MempoolTransactionExtended[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] { | ||||||
|  |   const auditPool: Map<string, MinerTransaction> = new Map(); | ||||||
|  |   const mempoolArray: MinerTransaction[] = []; | ||||||
|  | 
 | ||||||
|  |   candidates.forEach(tx => { | ||||||
|  |     // initializing everything up front helps V8 optimize property access later
 | ||||||
|  |     const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0))); | ||||||
|  |     const feePerVsize = (tx.fee / adjustedVsize); | ||||||
|  |     auditPool.set(tx.txid, { | ||||||
|  |       txid: tx.txid, | ||||||
|  |       order: txidToOrdering(tx.txid), | ||||||
|  |       fee: tx.fee, | ||||||
|  |       feeDelta: 0, | ||||||
|  |       weight: tx.weight, | ||||||
|  |       adjustedVsize, | ||||||
|  |       feePerVsize: feePerVsize, | ||||||
|  |       effectiveFeePerVsize: feePerVsize, | ||||||
|  |       dependencyRate: feePerVsize, | ||||||
|  |       sigops: tx.sigops || 0, | ||||||
|  |       inputs: (tx.vin?.map(vin => vin.txid) || []) as string[], | ||||||
|  |       relativesSet: false, | ||||||
|  |       ancestors: [], | ||||||
|  |       cluster: [], | ||||||
|  |       ancestorMap: new Map<string, MinerTransaction>(), | ||||||
|  |       children: new Set<MinerTransaction>(), | ||||||
|  |       ancestorFee: 0, | ||||||
|  |       ancestorVsize: 0, | ||||||
|  |       ancestorSigops: 0, | ||||||
|  |       score: 0, | ||||||
|  |       used: false, | ||||||
|  |       modified: false, | ||||||
|  |     }); | ||||||
|  |     mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // set accelerated effective fee
 | ||||||
|  |   for (const acceleration of accelerations) { | ||||||
|  |     const tx = auditPool.get(acceleration.txid); | ||||||
|  |     if (tx) { | ||||||
|  |       tx.feeDelta = acceleration.max_bid; | ||||||
|  |       tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize); | ||||||
|  |       tx.effectiveFeePerVsize = tx.feePerVsize; | ||||||
|  |       tx.dependencyRate = tx.feePerVsize; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Build relatives graph & calculate ancestor scores
 | ||||||
|  |   for (const tx of mempoolArray) { | ||||||
|  |     if (!tx.relativesSet) { | ||||||
|  |       setRelatives(tx, auditPool); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Sort by descending ancestor score
 | ||||||
|  |   mempoolArray.sort(priorityComparator); | ||||||
|  | 
 | ||||||
|  |   // Build blocks by greedily choosing the highest feerate package
 | ||||||
|  |   // (i.e. the package rooted in the transaction with the best ancestor score)
 | ||||||
|  |   const blocks: number[][] = []; | ||||||
|  |   let blockWeight = 0; | ||||||
|  |   let blockSigops = 0; | ||||||
|  |   const transactions: MinerTransaction[] = []; | ||||||
|  |   let modified: MinerTransaction[] = []; | ||||||
|  |   const overflow: MinerTransaction[] = []; | ||||||
|  |   let failures = 0; | ||||||
|  |   while (mempoolArray.length || modified.length) { | ||||||
|  |     // skip invalid transactions
 | ||||||
|  |     while (mempoolArray[0]?.used || mempoolArray[0]?.modified) { | ||||||
|  |       mempoolArray.shift(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Select best next package
 | ||||||
|  |     let nextTx; | ||||||
|  |     const nextPoolTx = mempoolArray[0]; | ||||||
|  |     const nextModifiedTx = modified[0]; | ||||||
|  |     if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) { | ||||||
|  |       nextTx = nextPoolTx; | ||||||
|  |       mempoolArray.shift(); | ||||||
|  |     } else { | ||||||
|  |       modified.shift(); | ||||||
|  |       if (nextModifiedTx) { | ||||||
|  |         nextTx = nextModifiedTx; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (nextTx && !nextTx?.used) { | ||||||
|  |       // Check if the package fits into this block
 | ||||||
|  |       if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) { | ||||||
|  |         const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values()); | ||||||
|  |         // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
 | ||||||
|  |         const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; | ||||||
|  |         const clusterTxids = sortedTxSet.map(tx => tx.txid); | ||||||
|  |         const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize); | ||||||
|  |         const used: MinerTransaction[] = []; | ||||||
|  |         while (sortedTxSet.length) { | ||||||
|  |           const ancestor = sortedTxSet.pop(); | ||||||
|  |           if (!ancestor) { | ||||||
|  |             continue; | ||||||
|  |           } | ||||||
|  |           ancestor.used = true; | ||||||
|  |           ancestor.usedBy = nextTx.txid; | ||||||
|  |           // update this tx with effective fee rate & relatives data
 | ||||||
|  |           if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) { | ||||||
|  |             ancestor.effectiveFeePerVsize = effectiveFeeRate; | ||||||
|  |           } | ||||||
|  |           ancestor.cluster = clusterTxids; | ||||||
|  |           transactions.push(ancestor); | ||||||
|  |           blockWeight += ancestor.weight; | ||||||
|  |           blockSigops += ancestor.sigops; | ||||||
|  |           used.push(ancestor); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // remove these as valid package ancestors for any descendants remaining in the mempool
 | ||||||
|  |         if (used.length) { | ||||||
|  |           used.forEach(tx => { | ||||||
|  |             modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         failures = 0; | ||||||
|  |       } else { | ||||||
|  |         // hold this package in an overflow list while we check for smaller options
 | ||||||
|  |         overflow.push(nextTx); | ||||||
|  |         failures++; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // this block is full
 | ||||||
|  |     const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000); | ||||||
|  |     const queueEmpty = !mempoolArray.length && !modified.length; | ||||||
|  | 
 | ||||||
|  |     if (exceededPackageTries || queueEmpty) { | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   for (const tx of transactions) { | ||||||
|  |     tx.ancestors = Object.values(tx.ancestorMap); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return transactions; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // traverse in-mempool ancestors
 | ||||||
|  | // recursion unavoidable, but should be limited to depth < 25 by mempool policy
 | ||||||
|  | function setRelatives( | ||||||
|  |   tx: MinerTransaction, | ||||||
|  |   mempool: Map<string, MinerTransaction>, | ||||||
|  | ): void { | ||||||
|  |   for (const parent of tx.inputs) { | ||||||
|  |     const parentTx = mempool.get(parent); | ||||||
|  |     if (parentTx && !tx.ancestorMap?.has(parent)) { | ||||||
|  |       tx.ancestorMap.set(parent, parentTx); | ||||||
|  |       parentTx.children.add(tx); | ||||||
|  |       // visit each node only once
 | ||||||
|  |       if (!parentTx.relativesSet) { | ||||||
|  |         setRelatives(parentTx, mempool); | ||||||
|  |       } | ||||||
|  |       parentTx.ancestorMap.forEach((ancestor) => { | ||||||
|  |         tx.ancestorMap.set(ancestor.txid, ancestor); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   tx.ancestorFee = (tx.fee + tx.feeDelta); | ||||||
|  |   tx.ancestorVsize = tx.adjustedVsize || 0; | ||||||
|  |   tx.ancestorSigops = tx.sigops || 0; | ||||||
|  |   tx.ancestorMap.forEach((ancestor) => { | ||||||
|  |     tx.ancestorFee += (ancestor.fee + ancestor.feeDelta); | ||||||
|  |     tx.ancestorVsize += ancestor.adjustedVsize; | ||||||
|  |     tx.ancestorSigops += ancestor.sigops; | ||||||
|  |   }); | ||||||
|  |   tx.score = tx.ancestorFee / tx.ancestorVsize; | ||||||
|  |   tx.relativesSet = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
 | ||||||
|  | // avoids recursion to limit call stack depth
 | ||||||
|  | function updateDescendants( | ||||||
|  |   rootTx: MinerTransaction, | ||||||
|  |   mempool: Map<string, MinerTransaction>, | ||||||
|  |   modified: MinerTransaction[], | ||||||
|  |   clusterRate: number, | ||||||
|  | ): MinerTransaction[] { | ||||||
|  |   const descendantSet: Set<MinerTransaction> = new Set(); | ||||||
|  |   // stack of nodes left to visit
 | ||||||
|  |   const descendants: MinerTransaction[] = []; | ||||||
|  |   let descendantTx: MinerTransaction | undefined; | ||||||
|  |   rootTx.children.forEach(childTx => { | ||||||
|  |     if (!descendantSet.has(childTx)) { | ||||||
|  |       descendants.push(childTx); | ||||||
|  |       descendantSet.add(childTx); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   while (descendants.length) { | ||||||
|  |     descendantTx = descendants.pop(); | ||||||
|  |     if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { | ||||||
|  |       // remove tx as ancestor
 | ||||||
|  |       descendantTx.ancestorMap.delete(rootTx.txid); | ||||||
|  |       descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta); | ||||||
|  |       descendantTx.ancestorVsize -= rootTx.adjustedVsize; | ||||||
|  |       descendantTx.ancestorSigops -= rootTx.sigops; | ||||||
|  |       descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize; | ||||||
|  |       descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate; | ||||||
|  | 
 | ||||||
|  |       if (!descendantTx.modified) { | ||||||
|  |         descendantTx.modified = true; | ||||||
|  |         modified.push(descendantTx); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // add this node's children to the stack
 | ||||||
|  |       descendantTx.children.forEach(childTx => { | ||||||
|  |         // visit each node only once
 | ||||||
|  |         if (!descendantSet.has(childTx)) { | ||||||
|  |           descendants.push(childTx); | ||||||
|  |           descendantSet.add(childTx); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // return new, resorted modified list
 | ||||||
|  |   return modified.sort(priorityComparator); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Used to sort an array of MinerTransactions by descending ancestor score
 | ||||||
|  | function priorityComparator(a: MinerTransaction, b: MinerTransaction): number { | ||||||
|  |   if (b.score === a.score) { | ||||||
|  |     // tie-break by txid for stability
 | ||||||
|  |     return a.order - b.order; | ||||||
|  |   } else { | ||||||
|  |     return b.score - a.score; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // returns the most significant 4 bytes of the txid as an integer
 | ||||||
|  | function txidToOrdering(txid: string): number { | ||||||
|  |   return parseInt( | ||||||
|  |     txid.substring(62, 64) + | ||||||
|  |       txid.substring(60, 62) + | ||||||
|  |       txid.substring(58, 60) + | ||||||
|  |       txid.substring(56, 58), | ||||||
|  |     16 | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -9,6 +9,7 @@ import bitcoinClient from '../bitcoin/bitcoin-client'; | |||||||
| import mining from "./mining"; | import mining from "./mining"; | ||||||
| import PricesRepository from '../../repositories/PricesRepository'; | import PricesRepository from '../../repositories/PricesRepository'; | ||||||
| import AccelerationRepository from '../../repositories/AccelerationRepository'; | import AccelerationRepository from '../../repositories/AccelerationRepository'; | ||||||
|  | import accelerationApi from '../services/acceleration'; | ||||||
| 
 | 
 | ||||||
| class MiningRoutes { | class MiningRoutes { | ||||||
|   public initRoutes(app: Application) { |   public initRoutes(app: Application) { | ||||||
| @ -24,6 +25,7 @@ class MiningRoutes { | |||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees', this.$getBlockFeesTimespan) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) |       .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) | ||||||
| @ -40,6 +42,8 @@ class MiningRoutes { | |||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight) |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/block/:height', this.$getAccelerationsByHeight) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations) |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/recent/:interval', this.$getRecentAccelerations) | ||||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals) |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations/total', this.$getAccelerationTotals) | ||||||
|  |       .get(config.MEMPOOL.API_URL_PREFIX + 'accelerations', this.$getActiveAccelerations) | ||||||
|  |       .post(config.MEMPOOL.API_URL_PREFIX + 'acceleration/request/:txid', this.$requestAcceleration) | ||||||
|     ; |     ; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -217,6 +221,24 @@ class MiningRoutes { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private async $getBlockFeesTimespan(req: Request, res: Response) { | ||||||
|  |     try { | ||||||
|  |       if (!parseInt(req.query.from as string, 10) || !parseInt(req.query.to as string, 10)) { | ||||||
|  |         throw new Error('Invalid timestamp range'); | ||||||
|  |       } | ||||||
|  |       if (parseInt(req.query.from as string, 10) > parseInt(req.query.to as string, 10)) { | ||||||
|  |         throw new Error('from must be less than to'); | ||||||
|  |       } | ||||||
|  |       const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10)); | ||||||
|  |       res.header('Pragma', 'public'); | ||||||
|  |       res.header('Cache-control', 'public'); | ||||||
|  |       res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); | ||||||
|  |       res.json(blockFees); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private async $getHistoricalBlockRewards(req: Request, res: Response) { |   private async $getHistoricalBlockRewards(req: Request, res: Response) { | ||||||
|     try { |     try { | ||||||
|       const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); |       const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); | ||||||
| @ -426,6 +448,33 @@ class MiningRoutes { | |||||||
|       res.status(500).send(e instanceof Error ? e.message : e); |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   private async $getActiveAccelerations(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()); | ||||||
|  |       if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { | ||||||
|  |         res.status(400).send('Acceleration data is not available.'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       res.status(200).send(accelerationApi.accelerations || []); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $requestAcceleration(req: Request, res: Response): Promise<void> { | ||||||
|  |     res.setHeader('Pragma', 'no-cache'); | ||||||
|  |     res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); | ||||||
|  |     res.setHeader('expires', -1); | ||||||
|  |     try { | ||||||
|  |       accelerationApi.accelerationRequested(req.params.txid); | ||||||
|  |       res.status(200).send(); | ||||||
|  |     } catch (e) { | ||||||
|  |       res.status(500).send(e instanceof Error ? e.message : e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new MiningRoutes(); | export default new MiningRoutes(); | ||||||
|  | |||||||
| @ -45,11 +45,22 @@ class Mining { | |||||||
|    */ |    */ | ||||||
|   public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> { |   public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> { | ||||||
|     return await BlocksRepository.$getHistoricalBlockFees( |     return await BlocksRepository.$getHistoricalBlockFees( | ||||||
|       this.getTimeRange(interval, 5), |       this.getTimeRange(interval), | ||||||
|       Common.getSqlInterval(interval) |       Common.getSqlInterval(interval) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Get timespan block total fees | ||||||
|  |    */ | ||||||
|  |   public async $getBlockFeesTimespan(from: number, to: number): Promise<number> { | ||||||
|  |     return await BlocksRepository.$getHistoricalBlockFees( | ||||||
|  |       this.getTimeRangeFromTimespan(from, to), | ||||||
|  |       null, | ||||||
|  |       {from, to} | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |    | ||||||
|   /** |   /** | ||||||
|    * Get historical block rewards |    * Get historical block rewards | ||||||
|    */ |    */ | ||||||
| @ -646,6 +657,24 @@ class Mining { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private getTimeRangeFromTimespan(from: number, to: number, scale = 1): number { | ||||||
|  |     const timespan = to - from; | ||||||
|  |     switch (true) { | ||||||
|  |       case timespan > 3600 * 24 * 365 * 4: return 86400 * scale; // 24h
 | ||||||
|  |       case timespan > 3600 * 24 * 365 * 3: return 43200 * scale; // 12h
 | ||||||
|  |       case timespan > 3600 * 24 * 365 * 2: return 43200 * scale; // 12h
 | ||||||
|  |       case timespan > 3600 * 24 * 365: return 28800 * scale; // 8h
 | ||||||
|  |       case timespan > 3600 * 24 * 30 * 6: return 28800 * scale; // 8h
 | ||||||
|  |       case timespan > 3600 * 24 * 30 * 3: return 10800 * scale; // 3h
 | ||||||
|  |       case timespan > 3600 * 24 * 30: return 7200 * scale; // 2h
 | ||||||
|  |       case timespan > 3600 * 24 * 7: return 1800 * scale; // 30min
 | ||||||
|  |       case timespan > 3600 * 24 * 3: return 300 * scale; // 5min
 | ||||||
|  |       case timespan > 3600 * 24: return 1 * scale; | ||||||
|  |       default: return 1 * scale; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  | 
 | ||||||
|   // Finds the oldest block in a consecutive chain back from the tip
 |   // Finds the oldest block in a consecutive chain back from the tip
 | ||||||
|   // assumes `blocks` is sorted in ascending height order
 |   // assumes `blocks` is sorted in ascending height order
 | ||||||
|   private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock { |   private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock { | ||||||
|  | |||||||
| @ -5,6 +5,9 @@ import PoolsRepository from '../repositories/PoolsRepository'; | |||||||
| import { PoolTag } from '../mempool.interfaces'; | import { PoolTag } from '../mempool.interfaces'; | ||||||
| import diskCache from './disk-cache'; | import diskCache from './disk-cache'; | ||||||
| import mining from './mining/mining'; | import mining from './mining/mining'; | ||||||
|  | import transactionUtils from './transaction-utils'; | ||||||
|  | import BlocksRepository from '../repositories/BlocksRepository'; | ||||||
|  | import redisCache from './redis-cache'; | ||||||
| 
 | 
 | ||||||
| class PoolsParser { | class PoolsParser { | ||||||
|   miningPools: any[] = []; |   miningPools: any[] = []; | ||||||
| @ -37,28 +40,53 @@ class PoolsParser { | |||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Populate our db with updated mining pool definition |    * Populate our db with updated mining pool definition | ||||||
|    * @param pools  |    * @param pools | ||||||
|    */ |    */ | ||||||
|   public async migratePoolsJson(): Promise<void> { |   public async migratePoolsJson(): Promise<void> { | ||||||
|     // We also need to wipe the backend cache to make sure we don't serve blocks with
 |     // We also need to wipe the backend cache to make sure we don't serve blocks with
 | ||||||
|     // the wrong mining pool (usually happen with unknown blocks)
 |     // the wrong mining pool (usually happen with unknown blocks)
 | ||||||
|     diskCache.setIgnoreBlocksCache(); |     diskCache.setIgnoreBlocksCache(); | ||||||
|  |     redisCache.setIgnoreBlocksCache(); | ||||||
| 
 | 
 | ||||||
|     await this.$insertUnknownPool(); |     await this.$insertUnknownPool(); | ||||||
| 
 | 
 | ||||||
|  |     let reindexUnknown = false; | ||||||
|  | 
 | ||||||
|     for (const pool of this.miningPools) { |     for (const pool of this.miningPools) { | ||||||
|       if (!pool.id) { |       if (!pool.id) { | ||||||
|         logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`); |         logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`); | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // One of the two fields 'addresses' or 'regexes' must be a non-empty array
 | ||||||
|  |       if (!pool.addresses && !pool.regexes) { | ||||||
|  |         logger.err(`Mining pool ${pool.name} must have at least one of the fields 'addresses' or 'regexes'. Skipping.`); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       pool.addresses = pool.addresses || []; | ||||||
|  |       pool.regexes = pool.regexes || []; | ||||||
|  | 
 | ||||||
|  |       if (pool.addresses.length === 0 && pool.regexes.length === 0) { | ||||||
|  |         logger.err(`Mining pool ${pool.name} has no 'addresses' nor 'regexes' defined. Skipping.`); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (pool.addresses.length === 0) { | ||||||
|  |         logger.warn(`Mining pool ${pool.name} has no 'addresses' defined.`); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (pool.regexes.length === 0) { | ||||||
|  |         logger.warn(`Mining pool ${pool.name} has no 'regexes' defined.`); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false); |       const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false); | ||||||
|       if (!poolDB) { |       if (!poolDB) { | ||||||
|         // New mining pool
 |         // New mining pool
 | ||||||
|         const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); |         const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); | ||||||
|         logger.debug(`Inserting new mining pool ${pool.name}`); |         logger.debug(`Inserting new mining pool ${pool.name}`); | ||||||
|         await PoolsRepository.$insertNewMiningPool(pool, slug); |         await PoolsRepository.$insertNewMiningPool(pool, slug); | ||||||
|         await this.$deleteUnknownBlocks(); |         reindexUnknown = true; | ||||||
|       } else { |       } else { | ||||||
|         if (poolDB.name !== pool.name) { |         if (poolDB.name !== pool.name) { | ||||||
|           // Pool has been renamed
 |           // Pool has been renamed
 | ||||||
| @ -76,7 +104,45 @@ class PoolsParser { | |||||||
|           // Pool addresses changed or coinbase tags changed
 |           // Pool addresses changed or coinbase tags changed
 | ||||||
|           logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`); |           logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`); | ||||||
|           await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes); |           await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes); | ||||||
|           await this.$deleteBlocksForPool(poolDB); |           reindexUnknown = true; | ||||||
|  |           await this.$reindexBlocksForPool(poolDB.id); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (reindexUnknown) { | ||||||
|  |       logger.notice(`Updating addresses and/or coinbase tags for unknown mining pool.`); | ||||||
|  |       let unknownPool; | ||||||
|  |       if (config.DATABASE.ENABLED === true) { | ||||||
|  |         unknownPool = await PoolsRepository.$getUnknownPool(); | ||||||
|  |       } else { | ||||||
|  |         unknownPool = this.unknownPool; | ||||||
|  |       } | ||||||
|  |       await this.$reindexBlocksForPool(unknownPool.id); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined { | ||||||
|  |     const asciiScriptSig = transactionUtils.hex2ascii(scriptsig); | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < pools.length; ++i) { | ||||||
|  |       if (addresses.length) { | ||||||
|  |         const poolAddresses: string[] = typeof pools[i].addresses === 'string' ? | ||||||
|  |           JSON.parse(pools[i].addresses) : pools[i].addresses; | ||||||
|  |         for (let y = 0; y < poolAddresses.length; y++) { | ||||||
|  |           if (addresses.indexOf(poolAddresses[y]) !== -1) { | ||||||
|  |             return pools[i]; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const regexes: string[] = typeof pools[i].regexes === 'string' ? | ||||||
|  |         JSON.parse(pools[i].regexes) : pools[i].regexes; | ||||||
|  |       for (let y = 0; y < regexes.length; ++y) { | ||||||
|  |         const regex = new RegExp(regexes[y], 'i'); | ||||||
|  |         const match = asciiScriptSig.match(regex); | ||||||
|  |         if (match !== null) { | ||||||
|  |           return pools[i]; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -112,68 +178,47 @@ class PoolsParser { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Delete indexed blocks for an updated mining pool |    * re-index pool assignment for blocks previously associated with pool | ||||||
|    *  |    * | ||||||
|    * @param pool  |    * @param pool local id of existing pool to reindex | ||||||
|    */ |    */ | ||||||
|   private async $deleteBlocksForPool(pool: PoolTag): Promise<void> { |   private async $reindexBlocksForPool(poolId: number): Promise<void> { | ||||||
|     // Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years
 |     let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
 | ||||||
|     // Ignore early days of Bitcoin as there were no mining pool yet
 |     if (config.MEMPOOL.NETWORK === 'testnet') { | ||||||
|     const [oldestPoolBlock]: any[] = await DB.query(` |       firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
 | ||||||
|       SELECT height |     } else if (config.MEMPOOL.NETWORK === 'signet') { | ||||||
|  |       firstKnownBlockPool = 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const [blocks]: any[] = await DB.query(` | ||||||
|  |       SELECT height, hash, coinbase_raw, coinbase_addresses | ||||||
|       FROM blocks |       FROM blocks | ||||||
|       WHERE pool_id = ? |       WHERE pool_id = ? | ||||||
|       ORDER BY height |       AND height >= ? | ||||||
|       LIMIT 1`,
 |       ORDER BY height DESC | ||||||
|       [pool.id] |     `, [poolId, firstKnownBlockPool]);
 | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
 |     let pools: PoolTag[] = []; | ||||||
|     if (config.MEMPOOL.NETWORK === 'testnet') { |     if (config.DATABASE.ENABLED === true) { | ||||||
|       firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
 |       pools = await PoolsRepository.$getPools(); | ||||||
|     } else if (config.MEMPOOL.NETWORK === 'signet') { |     } else { | ||||||
|       firstKnownBlockPool = 0; |       pools = this.miningPools; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : firstKnownBlockPool; |     let changed = 0; | ||||||
|     const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`); |     for (const block of blocks) { | ||||||
|     this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`); |       const addresses = JSON.parse(block.coinbase_addresses) || []; | ||||||
|     await DB.query(` |       const newPool = this.matchBlockMiner(block.coinbase_raw, addresses, pools); | ||||||
|       DELETE FROM blocks |       if (newPool && newPool.id !== poolId) { | ||||||
|       WHERE pool_id = ? AND height >= ${oldestBlockHeight}`,
 |         changed++; | ||||||
|       [unknownPool[0].id] |         await BlocksRepository.$savePool(block.hash, newPool.id); | ||||||
|     ); |       } | ||||||
|     logger.notice(`Deleting blocks from ${pool.name} mining pool for re-indexing`); |     } | ||||||
|     await DB.query(` | 
 | ||||||
|       DELETE FROM blocks |     logger.info(`${changed} blocks assigned to a new pool`, logger.tags.mining); | ||||||
|       WHERE pool_id = ?`,
 |  | ||||||
|       [pool.id] |  | ||||||
|     ); |  | ||||||
| 
 | 
 | ||||||
|     // Re-index hashrates and difficulty adjustments later
 |     // Re-index hashrates and difficulty adjustments later
 | ||||||
|     mining.reindexHashrateRequested = true; |     mining.reindexHashrateRequested = true; | ||||||
|     mining.reindexDifficultyAdjustmentRequested = true; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private async $deleteUnknownBlocks(): Promise<void> { |  | ||||||
|     let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52
 |  | ||||||
|     if (config.MEMPOOL.NETWORK === 'testnet') { |  | ||||||
|       firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581
 |  | ||||||
|     } else if (config.MEMPOOL.NETWORK === 'signet') { |  | ||||||
|       firstKnownBlockPool = 0; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`); |  | ||||||
|     this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${firstKnownBlockPool} for re-indexing`); |  | ||||||
|     await DB.query(` |  | ||||||
|       DELETE FROM blocks |  | ||||||
|       WHERE pool_id = ? AND height >= ${firstKnownBlockPool}`,
 |  | ||||||
|       [unknownPool[0].id] |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Re-index hashrates and difficulty adjustments later
 |  | ||||||
|     mining.reindexHashrateRequested = true; |  | ||||||
|     mining.reindexDifficultyAdjustmentRequested = true; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ class RedisCache { | |||||||
|   private rbfCacheQueue: { type: string, txid: string, value: any }[] = []; |   private rbfCacheQueue: { type: string, txid: string, value: any }[] = []; | ||||||
|   private rbfRemoveQueue: { type: string, txid: string }[] = []; |   private rbfRemoveQueue: { type: string, txid: string }[] = []; | ||||||
|   private txFlushLimit: number = 10000; |   private txFlushLimit: number = 10000; | ||||||
|  |   private ignoreBlocksCache = false; | ||||||
| 
 | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     if (config.REDIS.ENABLED) { |     if (config.REDIS.ENABLED) { | ||||||
| @ -155,7 +156,7 @@ class RedisCache { | |||||||
|     const toAdd = this.cacheQueue.slice(0, this.txFlushLimit); |     const toAdd = this.cacheQueue.slice(0, this.txFlushLimit); | ||||||
|     try { |     try { | ||||||
|       const msetData = toAdd.map(tx => { |       const msetData = toAdd.map(tx => { | ||||||
|         const minified: any = { ...tx }; |         const minified: any = structuredClone(tx); | ||||||
|         delete minified.hex; |         delete minified.hex; | ||||||
|         for (const vin of minified.vin) { |         for (const vin of minified.vin) { | ||||||
|           delete vin.inner_redeemscript_asm; |           delete vin.inner_redeemscript_asm; | ||||||
| @ -341,9 +342,7 @@ class RedisCache { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     logger.info('Restoring mempool and blocks data from Redis cache'); |     logger.info('Restoring mempool and blocks data from Redis cache'); | ||||||
|     // Load block data
 | 
 | ||||||
|     const loadedBlocks = await this.$getBlocks(); |  | ||||||
|     const loadedBlockSummaries = await this.$getBlockSummaries(); |  | ||||||
|     // Load mempool
 |     // Load mempool
 | ||||||
|     const loadedMempool = await this.$getMempool(); |     const loadedMempool = await this.$getMempool(); | ||||||
|     this.inflateLoadedTxs(loadedMempool); |     this.inflateLoadedTxs(loadedMempool); | ||||||
| @ -352,9 +351,14 @@ class RedisCache { | |||||||
|     const rbfTrees = await this.$getRbfEntries('tree'); |     const rbfTrees = await this.$getRbfEntries('tree'); | ||||||
|     const rbfExpirations = await this.$getRbfEntries('exp'); |     const rbfExpirations = await this.$getRbfEntries('exp'); | ||||||
| 
 | 
 | ||||||
|     // Set loaded data
 |     // Load & set block data
 | ||||||
|     blocks.setBlocks(loadedBlocks || []); |     if (!this.ignoreBlocksCache) { | ||||||
|     blocks.setBlockSummaries(loadedBlockSummaries || []); |       const loadedBlocks = await this.$getBlocks(); | ||||||
|  |       const loadedBlockSummaries = await this.$getBlockSummaries(); | ||||||
|  |       blocks.setBlocks(loadedBlocks || []); | ||||||
|  |       blocks.setBlockSummaries(loadedBlockSummaries || []); | ||||||
|  |     } | ||||||
|  |     // Set other data
 | ||||||
|     await memPool.$setMempool(loadedMempool); |     await memPool.$setMempool(loadedMempool); | ||||||
|     await rbfCache.load({ |     await rbfCache.load({ | ||||||
|       txs: rbfTxs, |       txs: rbfTxs, | ||||||
| @ -411,6 +415,10 @@ class RedisCache { | |||||||
|     } |     } | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   public setIgnoreBlocksCache(): void { | ||||||
|  |     this.ignoreBlocksCache = true; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new RedisCache(); | export default new RedisCache(); | ||||||
|  | |||||||
| @ -1,12 +1,23 @@ | |||||||
| import config from '../../config'; | import config from '../../config'; | ||||||
| import logger from '../../logger'; | import logger from '../../logger'; | ||||||
| import { BlockExtended, PoolTag } from '../../mempool.interfaces'; | import { BlockExtended } from '../../mempool.interfaces'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| 
 | 
 | ||||||
|  | type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; | ||||||
|  | 
 | ||||||
| export interface Acceleration { | export interface Acceleration { | ||||||
|   txid: string, |   txid: string, | ||||||
|  |   added: number, | ||||||
|  |   effectiveVsize: number, | ||||||
|  |   effectiveFee: number, | ||||||
|   feeDelta: number, |   feeDelta: number, | ||||||
|   pools: number[], |   pools: number[], | ||||||
|  |   positions?: { | ||||||
|  |     [pool: number]: { | ||||||
|  |       block: number, | ||||||
|  |       vbytes: number, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export interface AccelerationHistory { | export interface AccelerationHistory { | ||||||
| @ -22,25 +33,95 @@ export interface AccelerationHistory { | |||||||
|   feeDelta: number, |   feeDelta: number, | ||||||
|   blockHash: string, |   blockHash: string, | ||||||
|   blockHeight: number, |   blockHeight: number, | ||||||
|   pools: { |   pools: number[]; | ||||||
|     pool_unique_id: number, |  | ||||||
|     username: string, |  | ||||||
|   }[], |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class AccelerationApi { | class AccelerationApi { | ||||||
|   public async $fetchAccelerations(): Promise<Acceleration[] | null> { |   private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS; | ||||||
|     if (config.MEMPOOL_SERVICES.ACCELERATIONS) { |   private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); | ||||||
|       try { |   private _accelerations: Acceleration[] | null = null; | ||||||
|         const response = await axios.get(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`, { responseType: 'json', timeout: 10000 }); |   private lastPoll = 0; | ||||||
|         return response.data as Acceleration[]; |   private forcePoll = false; | ||||||
|       } catch (e) { |   private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {}; | ||||||
|         logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); | 
 | ||||||
|         return null; |   public get accelerations(): Acceleration[] | null { | ||||||
|  |     return this._accelerations; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public countMyAccelerationsWithStatus(filter: MyAccelerationStatus): number { | ||||||
|  |     return Object.values(this.myAccelerations).reduce((count, {status}) => { return count + (status === filter ? 1 : 0); }, 0); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public accelerationRequested(txid: string): void { | ||||||
|  |     if (this.onDemandPollingEnabled) { | ||||||
|  |       this.myAccelerations[txid] = { status: 'requested', added: Date.now() }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public accelerationConfirmed(): void { | ||||||
|  |     this.forcePoll = true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $fetchAccelerations(): Promise<Acceleration[] | null> { | ||||||
|  |     try { | ||||||
|  |       const response = await axios.get(this.apiPath, { responseType: 'json', timeout: 10000 }); | ||||||
|  |       return response?.data || []; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.warn('Failed to fetch current accelerations from the mempool services backend: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $updateAccelerations(): Promise<Acceleration[] | null> { | ||||||
|  |     if (!this.onDemandPollingEnabled) { | ||||||
|  |       const accelerations = await this.$fetchAccelerations(); | ||||||
|  |       if (accelerations) { | ||||||
|  |         this._accelerations = accelerations; | ||||||
|  |         return this._accelerations; | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       return []; |       return this.$updateAccelerationsOnDemand(); | ||||||
|     } |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> { | ||||||
|  |     const shouldUpdate = this.forcePoll | ||||||
|  |       || this.countMyAccelerationsWithStatus('requested') > 0 | ||||||
|  |       || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); | ||||||
|  | 
 | ||||||
|  |     // update accelerations if necessary
 | ||||||
|  |     if (shouldUpdate) { | ||||||
|  |       const accelerations = await this.$fetchAccelerations(); | ||||||
|  |       this.lastPoll = Date.now(); | ||||||
|  |       this.forcePoll = false; | ||||||
|  |       if (accelerations) { | ||||||
|  |         const latestAccelerations: Record<string, Acceleration> = {}; | ||||||
|  |         // set relevant accelerations to 'accelerating'
 | ||||||
|  |         for (const acc of accelerations) { | ||||||
|  |           if (this.myAccelerations[acc.txid]) { | ||||||
|  |             latestAccelerations[acc.txid] = acc; | ||||||
|  |             this.myAccelerations[acc.txid] = { status: 'accelerating', added: Date.now(), acceleration: acc }; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         // txs that are no longer accelerating are either confirmed or canceled, so mark for expiry
 | ||||||
|  |         for (const [txid, { status, acceleration }] of Object.entries(this.myAccelerations)) { | ||||||
|  |           if (status === 'accelerating' && !latestAccelerations[txid]) { | ||||||
|  |             this.myAccelerations[txid] = { status: 'done', added: Date.now(), acceleration }; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // clear expired accelerations (confirmed / failed / not accepted) after 10 minutes
 | ||||||
|  |     for (const [txid, { status, added }] of Object.entries(this.myAccelerations)) { | ||||||
|  |       if (['requested', 'done'].includes(status) && added < (Date.now() - (1000 * 60 * 10))) { | ||||||
|  |         delete this.myAccelerations[txid]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; | ||||||
|  |     return this._accelerations; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> { |   public async $fetchAccelerationHistory(page?: number, status?: string): Promise<AccelerationHistory[] | null> { | ||||||
|  | |||||||
| @ -64,7 +64,7 @@ class StatisticsApi { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $create(statistics: Statistic): Promise<number | undefined> { |   public async $create(statistics: Statistic, convertToDatetime = false): Promise<number | undefined> { | ||||||
|     try { |     try { | ||||||
|       const query = `INSERT INTO statistics(
 |       const query = `INSERT INTO statistics(
 | ||||||
|               added, |               added, | ||||||
| @ -114,7 +114,7 @@ class StatisticsApi { | |||||||
|               vsize_1800, |               vsize_1800, | ||||||
|               vsize_2000 |               vsize_2000 | ||||||
|             ) |             ) | ||||||
|             VALUES (${statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, |             VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, | ||||||
|                ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
 |                ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
 | ||||||
| 
 | 
 | ||||||
|       const params: (string | number)[] = [ |       const params: (string | number)[] = [ | ||||||
| @ -456,6 +456,59 @@ class StatisticsApi { | |||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   public mapOptimizedStatisticToStatistic(statistic: OptimizedStatistic[]): Statistic[] { | ||||||
|  |     return statistic.map((s) => { | ||||||
|  |       return { | ||||||
|  |         added: s.added, | ||||||
|  |         unconfirmed_transactions: s.count, | ||||||
|  |         tx_per_second: 0, | ||||||
|  |         vbytes_per_second: s.vbytes_per_second, | ||||||
|  |         mempool_byte_weight: s.mempool_byte_weight || 0, | ||||||
|  |         total_fee: s.total_fee || 0, | ||||||
|  |         min_fee: s.min_fee, | ||||||
|  |         fee_data: '', | ||||||
|  |         vsize_1: s.vsizes[0], | ||||||
|  |         vsize_2: s.vsizes[1], | ||||||
|  |         vsize_3: s.vsizes[2], | ||||||
|  |         vsize_4: s.vsizes[3], | ||||||
|  |         vsize_5: s.vsizes[4], | ||||||
|  |         vsize_6: s.vsizes[5], | ||||||
|  |         vsize_8: s.vsizes[6], | ||||||
|  |         vsize_10: s.vsizes[7], | ||||||
|  |         vsize_12: s.vsizes[8], | ||||||
|  |         vsize_15: s.vsizes[9], | ||||||
|  |         vsize_20: s.vsizes[10], | ||||||
|  |         vsize_30: s.vsizes[11], | ||||||
|  |         vsize_40: s.vsizes[12], | ||||||
|  |         vsize_50: s.vsizes[13], | ||||||
|  |         vsize_60: s.vsizes[14], | ||||||
|  |         vsize_70: s.vsizes[15], | ||||||
|  |         vsize_80: s.vsizes[16], | ||||||
|  |         vsize_90: s.vsizes[17], | ||||||
|  |         vsize_100: s.vsizes[18], | ||||||
|  |         vsize_125: s.vsizes[19], | ||||||
|  |         vsize_150: s.vsizes[20], | ||||||
|  |         vsize_175: s.vsizes[21], | ||||||
|  |         vsize_200: s.vsizes[22], | ||||||
|  |         vsize_250: s.vsizes[23], | ||||||
|  |         vsize_300: s.vsizes[24], | ||||||
|  |         vsize_350: s.vsizes[25], | ||||||
|  |         vsize_400: s.vsizes[26], | ||||||
|  |         vsize_500: s.vsizes[27], | ||||||
|  |         vsize_600: s.vsizes[28], | ||||||
|  |         vsize_700: s.vsizes[29], | ||||||
|  |         vsize_800: s.vsizes[30], | ||||||
|  |         vsize_900: s.vsizes[31], | ||||||
|  |         vsize_1000: s.vsizes[32], | ||||||
|  |         vsize_1200: s.vsizes[33], | ||||||
|  |         vsize_1400: s.vsizes[34], | ||||||
|  |         vsize_1600: s.vsizes[35], | ||||||
|  |         vsize_1800: s.vsizes[36], | ||||||
|  |         vsize_2000: s.vsizes[37], | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new StatisticsApi(); | export default new StatisticsApi(); | ||||||
|  | |||||||
| @ -103,7 +103,7 @@ class TransactionUtils { | |||||||
|     } |     } | ||||||
|     const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4); |     const feePerVbytes = (transaction.fee || 0) / (transaction.weight / 4); | ||||||
|     const transactionExtended: TransactionExtended = Object.assign({ |     const transactionExtended: TransactionExtended = Object.assign({ | ||||||
|       vsize: Math.round(transaction.weight / 4), |       vsize: transaction.weight / 4, | ||||||
|       feePerVsize: feePerVbytes, |       feePerVsize: feePerVbytes, | ||||||
|       effectiveFeePerVsize: feePerVbytes, |       effectiveFeePerVsize: feePerVbytes, | ||||||
|     }, transaction); |     }, transaction); | ||||||
| @ -123,7 +123,7 @@ class TransactionUtils { | |||||||
|     const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; |     const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; | ||||||
|     const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { |     const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { | ||||||
|       order: this.txidToOrdering(transaction.txid), |       order: this.txidToOrdering(transaction.txid), | ||||||
|       vsize: Math.round(transaction.weight / 4), |       vsize, | ||||||
|       adjustedVsize, |       adjustedVsize, | ||||||
|       sigops, |       sigops, | ||||||
|       feePerVsize: feePerVbytes, |       feePerVsize: feePerVbytes, | ||||||
| @ -338,6 +338,87 @@ class TransactionUtils { | |||||||
|     const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; |     const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; | ||||||
|     return witness[positionOfScript]; |     return witness[positionOfScript]; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // calculate the most parsimonious set of prioritizations given a list of block transactions
 | ||||||
|  |   // (i.e. the most likely prioritizations and deprioritizations)
 | ||||||
|  |   public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } { | ||||||
|  |     // find the longest increasing subsequence of transactions
 | ||||||
|  |     // (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
 | ||||||
|  |     // should be O(n log n)
 | ||||||
|  |     const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
 | ||||||
|  |     if (X.length < 2) { | ||||||
|  |       return { prioritized: [], deprioritized: [] }; | ||||||
|  |     } | ||||||
|  |     const N = X.length; | ||||||
|  |     const P: number[] = new Array(N); | ||||||
|  |     const M: number[] = new Array(N + 1); | ||||||
|  |     M[0] = -1; // undefined so can be set to any value
 | ||||||
|  | 
 | ||||||
|  |     let L = 0; | ||||||
|  |     for (let i = 0; i < N; i++) { | ||||||
|  |       // Binary search for the smallest positive l ≤ L
 | ||||||
|  |       // such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
 | ||||||
|  |       let lo = 1; | ||||||
|  |       let hi = L + 1; | ||||||
|  |       while (lo < hi) { | ||||||
|  |         const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
 | ||||||
|  |         if (X[M[mid]].rate > X[i].rate) { | ||||||
|  |           hi = mid; | ||||||
|  |         } else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
 | ||||||
|  |           lo = mid + 1; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // After searching, lo == hi is 1 greater than the
 | ||||||
|  |       // length of the longest prefix of X[i]
 | ||||||
|  |       const newL = lo; | ||||||
|  | 
 | ||||||
|  |       // The predecessor of X[i] is the last index of
 | ||||||
|  |       // the subsequence of length newL-1
 | ||||||
|  |       P[i] = M[newL - 1]; | ||||||
|  |       M[newL] = i; | ||||||
|  | 
 | ||||||
|  |       if (newL > L) { | ||||||
|  |         // If we found a subsequence longer than any we've
 | ||||||
|  |         // found yet, update L
 | ||||||
|  |         L = newL; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Reconstruct the longest increasing subsequence
 | ||||||
|  |     // It consists of the values of X at the L indices:
 | ||||||
|  |     // ..., P[P[M[L]]], P[M[L]], M[L]
 | ||||||
|  |     const LIS: any[] = new Array(L); | ||||||
|  |     let k = M[L]; | ||||||
|  |     for (let j = L - 1; j >= 0; j--) { | ||||||
|  |       LIS[j] = X[k]; | ||||||
|  |       k = P[k]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const lisMap = new Map<string, number>(); | ||||||
|  |     LIS.forEach((tx, index) => lisMap.set(tx.txid, index)); | ||||||
|  | 
 | ||||||
|  |     const prioritized: string[] = []; | ||||||
|  |     const deprioritized: string[] = []; | ||||||
|  | 
 | ||||||
|  |     let lastRate = X[0].rate; | ||||||
|  | 
 | ||||||
|  |     for (const tx of X) { | ||||||
|  |       if (lisMap.has(tx.txid)) { | ||||||
|  |         lastRate = tx.rate; | ||||||
|  |       } else { | ||||||
|  |         if (Math.abs(tx.rate - lastRate) < 0.1) { | ||||||
|  |           // skip if the rate is almost the same as the previous transaction
 | ||||||
|  |         } else if (tx.rate <= lastRate) { | ||||||
|  |           prioritized.push(tx.txid); | ||||||
|  |         } else { | ||||||
|  |           deprioritized.push(tx.txid); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { prioritized, deprioritized }; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new TransactionUtils(); | export default new TransactionUtils(); | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import * as WebSocket from 'ws'; | |||||||
| import { | import { | ||||||
|   BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, |   BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, | ||||||
|   OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, |   OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, | ||||||
|  |   MempoolDelta, MempoolDeltaTxids | ||||||
| } from '../mempool.interfaces'; | } from '../mempool.interfaces'; | ||||||
| import blocks from './blocks'; | import blocks from './blocks'; | ||||||
| import memPool from './mempool'; | import memPool from './mempool'; | ||||||
| @ -32,7 +33,7 @@ interface AddressTransactions { | |||||||
|   removed: MempoolTransactionExtended[], |   removed: MempoolTransactionExtended[], | ||||||
| } | } | ||||||
| import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; | import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; | ||||||
| import { calculateCpfp } from './cpfp'; | import { calculateMempoolTxCpfp } from './cpfp'; | ||||||
| 
 | 
 | ||||||
| // valid 'want' subscriptions
 | // valid 'want' subscriptions
 | ||||||
| const wantable = [ | const wantable = [ | ||||||
| @ -44,7 +45,7 @@ const wantable = [ | |||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| class WebsocketHandler { | class WebsocketHandler { | ||||||
|   private wss: WebSocket.Server | undefined; |   private webSocketServers: WebSocket.Server[] = []; | ||||||
|   private extraInitProperties = {}; |   private extraInitProperties = {}; | ||||||
| 
 | 
 | ||||||
|   private numClients = 0; |   private numClients = 0; | ||||||
| @ -54,11 +55,12 @@ class WebsocketHandler { | |||||||
|   private socketData: { [key: string]: string } = {}; |   private socketData: { [key: string]: string } = {}; | ||||||
|   private serializedInitData: string = '{}'; |   private serializedInitData: string = '{}'; | ||||||
|   private lastRbfSummary: ReplacementInfo[] | null = null; |   private lastRbfSummary: ReplacementInfo[] | null = null; | ||||||
|  |   private mempoolSequence: number = 0; | ||||||
| 
 | 
 | ||||||
|   constructor() { } |   constructor() { } | ||||||
| 
 | 
 | ||||||
|   setWebsocketServer(wss: WebSocket.Server) { |   addWebsocketServer(wss: WebSocket.Server) { | ||||||
|     this.wss = wss; |     this.webSocketServers.push(wss); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setExtraInitData(property: string, value: any) { |   setExtraInitData(property: string, value: any) { | ||||||
| @ -102,11 +104,13 @@ class WebsocketHandler { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setupConnectionHandling() { |   setupConnectionHandling() { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.wss.on('connection', (client: WebSocket, req) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.on('connection', (client: WebSocket, req) => { | ||||||
|       this.numConnected++; |       this.numConnected++; | ||||||
|       client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown'; |       client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown'; | ||||||
|       client.on('error', (e) => { |       client.on('error', (e) => { | ||||||
| @ -202,7 +206,8 @@ class WebsocketHandler { | |||||||
|                 } |                 } | ||||||
|                 response['txPosition'] = JSON.stringify({ |                 response['txPosition'] = JSON.stringify({ | ||||||
|                   txid: trackTxid, |                   txid: trackTxid, | ||||||
|                   position |                   position, | ||||||
|  |                   accelerationPositions: memPool.getAccelerationPositions(tx.txid), | ||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
|             } else { |             } else { | ||||||
| @ -315,6 +320,7 @@ class WebsocketHandler { | |||||||
|               const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); |               const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||||
|               response['projected-block-transactions'] = JSON.stringify({ |               response['projected-block-transactions'] = JSON.stringify({ | ||||||
|                 index: index, |                 index: index, | ||||||
|  |                 sequence: this.mempoolSequence, | ||||||
|                 blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx), |                 blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx), | ||||||
|               }); |               }); | ||||||
|             } else { |             } else { | ||||||
| @ -342,6 +348,17 @@ class WebsocketHandler { | |||||||
|             } |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|  |           if (parsedMessage && parsedMessage['track-accelerations'] != null) { | ||||||
|  |             if (parsedMessage['track-accelerations']) { | ||||||
|  |               client['track-accelerations'] = true; | ||||||
|  |               response['accelerations'] = JSON.stringify({ | ||||||
|  |                 accelerations: Object.values(memPool.getAccelerations()), | ||||||
|  |               }); | ||||||
|  |             } else { | ||||||
|  |               client['track-accelerations'] = false; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|           if (parsedMessage.action === 'init') { |           if (parsedMessage.action === 'init') { | ||||||
|             if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { |             if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { | ||||||
|               this.updateSocketData(); |               this.updateSocketData(); | ||||||
| @ -360,6 +377,18 @@ class WebsocketHandler { | |||||||
|             client['track-donation'] = parsedMessage['track-donation']; |             client['track-donation'] = parsedMessage['track-donation']; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|  |           if (parsedMessage['track-mempool-txids'] === true) { | ||||||
|  |             client['track-mempool-txids'] = true; | ||||||
|  |           } else if (parsedMessage['track-mempool-txids'] === false) { | ||||||
|  |             delete client['track-mempool-txids']; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (parsedMessage['track-mempool'] === true) { | ||||||
|  |             client['track-mempool'] = true; | ||||||
|  |           } else if (parsedMessage['track-mempool'] === false) { | ||||||
|  |             delete client['track-mempool']; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|           if (Object.keys(response).length) { |           if (Object.keys(response).length) { | ||||||
|             client.send(this.serializeResponse(response)); |             client.send(this.serializeResponse(response)); | ||||||
|           } |           } | ||||||
| @ -369,14 +398,17 @@ class WebsocketHandler { | |||||||
|         } |         } | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleNewDonation(id: string) { |   handleNewDonation(id: string) { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -384,43 +416,50 @@ class WebsocketHandler { | |||||||
|         client.send(JSON.stringify({ donationConfirmed: true })); |         client.send(JSON.stringify({ donationConfirmed: true })); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleLoadingChanged(indicators: ILoadingIndicators) { |   handleLoadingChanged(indicators: ILoadingIndicators) { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.updateSocketDataFields({ 'loadingIndicators': indicators }); |     this.updateSocketDataFields({ 'loadingIndicators': indicators }); | ||||||
| 
 | 
 | ||||||
|     const response = JSON.stringify({ loadingIndicators: indicators }); |     const response = JSON.stringify({ loadingIndicators: indicators }); | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       client.send(response); |       client.send(response); | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleNewConversionRates(conversionRates: ApiPrice) { |   handleNewConversionRates(conversionRates: ApiPrice) { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.updateSocketDataFields({ 'conversions': conversionRates }); |     this.updateSocketDataFields({ 'conversions': conversionRates }); | ||||||
| 
 | 
 | ||||||
|     const response = JSON.stringify({ conversions: conversionRates }); |     const response = JSON.stringify({ conversions: conversionRates }); | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       client.send(response); |       client.send(response); | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleNewStatistic(stats: OptimizedStatistic) { |   handleNewStatistic(stats: OptimizedStatistic) { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.printLogs(); |     this.printLogs(); | ||||||
| @ -429,7 +468,9 @@ class WebsocketHandler { | |||||||
|       'live-2h-chart': stats |       'live-2h-chart': stats | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -440,11 +481,12 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|       client.send(response); |       client.send(response); | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleReorg(): void { |   handleReorg(): void { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const da = difficultyAdjustment.getDifficultyAdjustment(); |     const da = difficultyAdjustment.getDifficultyAdjustment(); | ||||||
| @ -455,7 +497,9 @@ class WebsocketHandler { | |||||||
|       'da': da?.previousTime ? da : undefined, |       'da': da?.previousTime ? da : undefined, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -473,13 +517,14 @@ class WebsocketHandler { | |||||||
|         client.send(this.serializeResponse(response)); |         client.send(this.serializeResponse(response)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, |   async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, | ||||||
|     newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], |     newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], | ||||||
|     candidates?: GbtCandidates): Promise<void> { |     candidates?: GbtCandidates): Promise<void> { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.printLogs(); |     this.printLogs(); | ||||||
| @ -493,9 +538,9 @@ class WebsocketHandler { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (config.MEMPOOL.RUST_GBT) { |     if (config.MEMPOOL.RUST_GBT) { | ||||||
|       await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, config.MEMPOOL_SERVICES.ACCELERATIONS); |       await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, newMempool, added, removed, candidates, true); | ||||||
|     } else { |     } else { | ||||||
|       await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS); |       await mempoolBlocks.$updateBlockTemplates(transactionIds, newMempool, added, removed, candidates, accelerationDelta, true, true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const mBlocks = mempoolBlocks.getMempoolBlocks(); |     const mBlocks = mempoolBlocks.getMempoolBlocks(); | ||||||
| @ -504,6 +549,7 @@ class WebsocketHandler { | |||||||
|     const vBytesPerSecond = memPool.getVBytesPerSecond(); |     const vBytesPerSecond = memPool.getVBytesPerSecond(); | ||||||
|     const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); |     const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); | ||||||
|     const da = difficultyAdjustment.getDifficultyAdjustment(); |     const da = difficultyAdjustment.getDifficultyAdjustment(); | ||||||
|  |     const accelerations = memPool.getAccelerations(); | ||||||
|     memPool.handleRbfTransactions(rbfTransactions); |     memPool.handleRbfTransactions(rbfTransactions); | ||||||
|     const rbfChanges = rbfCache.getRbfChanges(); |     const rbfChanges = rbfCache.getRbfChanges(); | ||||||
|     let rbfReplacements; |     let rbfReplacements; | ||||||
| @ -525,6 +571,33 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|     const latestTransactions = memPool.getLatestTransactions(); |     const latestTransactions = memPool.getLatestTransactions(); | ||||||
| 
 | 
 | ||||||
|  |     if (memPool.isInSync()) { | ||||||
|  |       this.mempoolSequence++; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; | ||||||
|  |     for (const tx of newTransactions) { | ||||||
|  |       if (rbfTransactions[tx.txid]) { | ||||||
|  |         for (const replaced of rbfTransactions[tx.txid]) { | ||||||
|  |           replacedTransactions.push({ replaced: replaced.txid, by: tx }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const mempoolDeltaTxids: MempoolDeltaTxids = { | ||||||
|  |       sequence: this.mempoolSequence, | ||||||
|  |       added: newTransactions.map(tx => tx.txid), | ||||||
|  |       removed: deletedTransactions.map(tx => tx.txid), | ||||||
|  |       mined: [], | ||||||
|  |       replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), | ||||||
|  |     }; | ||||||
|  |     const mempoolDelta: MempoolDelta = { | ||||||
|  |       sequence: this.mempoolSequence, | ||||||
|  |       added: newTransactions, | ||||||
|  |       removed: deletedTransactions.map(tx => tx.txid), | ||||||
|  |       mined: [], | ||||||
|  |       replaced: replacedTransactions, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     // update init data
 |     // update init data
 | ||||||
|     const socketDataFields = { |     const socketDataFields = { | ||||||
|       'mempoolInfo': mempoolInfo, |       'mempoolInfo': mempoolInfo, | ||||||
| @ -552,7 +625,9 @@ class WebsocketHandler { | |||||||
|     // pre-compute new tracked outspends
 |     // pre-compute new tracked outspends
 | ||||||
|     const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {}; |     const outspendCache: { [txid: string]: { [vout: number]: { vin: number, txid: string } } } = {}; | ||||||
|     const trackedTxs = new Set<string>(); |     const trackedTxs = new Set<string>(); | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client['track-tx']) { |       if (client['track-tx']) { | ||||||
|         trackedTxs.add(client['track-tx']); |         trackedTxs.add(client['track-tx']); | ||||||
|       } |       } | ||||||
| @ -562,6 +637,7 @@ class WebsocketHandler { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|     if (trackedTxs.size > 0) { |     if (trackedTxs.size > 0) { | ||||||
|       for (const tx of newTransactions) { |       for (const tx of newTransactions) { | ||||||
|         for (let i = 0; i < tx.vin.length; i++) { |         for (let i = 0; i < tx.vin.length; i++) { | ||||||
| @ -581,7 +657,15 @@ class WebsocketHandler { | |||||||
|     const addressCache = this.makeAddressCache(newTransactions); |     const addressCache = this.makeAddressCache(newTransactions); | ||||||
|     const removedAddressCache = this.makeAddressCache(deletedTransactions); |     const removedAddressCache = this.makeAddressCache(deletedTransactions); | ||||||
| 
 | 
 | ||||||
|     this.wss.clients.forEach(async (client) => { |     // pre-compute acceleration delta
 | ||||||
|  |     const accelerationUpdate = { | ||||||
|  |       added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), | ||||||
|  |       removed: accelerationDelta.filter(txid => !accelerations[txid]), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach(async (client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -737,10 +821,14 @@ class WebsocketHandler { | |||||||
|             position: { |             position: { | ||||||
|               ...mempoolTx.position, |               ...mempoolTx.position, | ||||||
|               accelerated: mempoolTx.acceleration || undefined, |               accelerated: mempoolTx.acceleration || undefined, | ||||||
|             } |               acceleratedBy: mempoolTx.acceleratedBy || undefined, | ||||||
|  |               acceleratedAt: mempoolTx.acceleratedAt || undefined, | ||||||
|  |               feeDelta: mempoolTx.feeDelta || undefined, | ||||||
|  |             }, | ||||||
|  |             accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), | ||||||
|           }; |           }; | ||||||
|           if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { |           if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) { | ||||||
|             calculateCpfp(mempoolTx, newMempool); |             calculateMempoolTxCpfp(mempoolTx, newMempool); | ||||||
|           } |           } | ||||||
|           if (mempoolTx.cpfpDirty) { |           if (mempoolTx.cpfpDirty) { | ||||||
|             positionData['cpfp'] = { |             positionData['cpfp'] = { | ||||||
| @ -750,7 +838,7 @@ class WebsocketHandler { | |||||||
|               effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null, |               effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null, | ||||||
|               sigops: mempoolTx.sigops, |               sigops: mempoolTx.sigops, | ||||||
|               adjustedVsize: mempoolTx.adjustedVsize, |               adjustedVsize: mempoolTx.adjustedVsize, | ||||||
|               acceleration: mempoolTx.acceleration |               acceleration: mempoolTx.acceleration, | ||||||
|             }; |             }; | ||||||
|           } |           } | ||||||
|           response['txPosition'] = JSON.stringify(positionData); |           response['txPosition'] = JSON.stringify(positionData); | ||||||
| @ -775,9 +863,12 @@ class WebsocketHandler { | |||||||
|             txInfo.position = { |             txInfo.position = { | ||||||
|               ...mempoolTx.position, |               ...mempoolTx.position, | ||||||
|               accelerated: mempoolTx.acceleration || undefined, |               accelerated: mempoolTx.acceleration || undefined, | ||||||
|  |               acceleratedBy: mempoolTx.acceleratedBy || undefined, | ||||||
|  |               acceleratedAt: mempoolTx.acceleratedAt || undefined, | ||||||
|  |               feeDelta: mempoolTx.feeDelta || undefined, | ||||||
|             }; |             }; | ||||||
|             if (!mempoolTx.cpfpChecked) { |             if (!mempoolTx.cpfpChecked) { | ||||||
|               calculateCpfp(mempoolTx, newMempool); |               calculateMempoolTxCpfp(mempoolTx, newMempool); | ||||||
|             } |             } | ||||||
|             if (mempoolTx.cpfpDirty) { |             if (mempoolTx.cpfpDirty) { | ||||||
|               txInfo.cpfp = { |               txInfo.cpfp = { | ||||||
| @ -802,6 +893,7 @@ class WebsocketHandler { | |||||||
|         if (mBlockDeltas[index]) { |         if (mBlockDeltas[index]) { | ||||||
|           response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, { |           response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, { | ||||||
|             index: index, |             index: index, | ||||||
|  |             sequence: this.mempoolSequence, | ||||||
|             delta: mBlockDeltas[index], |             delta: mBlockDeltas[index], | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
| @ -817,17 +909,32 @@ class WebsocketHandler { | |||||||
|         response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); |         response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (client['track-mempool-txids']) { | ||||||
|  |         response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (client['track-mempool']) { | ||||||
|  |         response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) { | ||||||
|  |         response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (Object.keys(response).length) { |       if (Object.keys(response).length) { | ||||||
|         client.send(this.serializeResponse(response)); |         client.send(this.serializeResponse(response)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|   |   | ||||||
|   async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> { |   async handleNewBlock(block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]): Promise<void> { | ||||||
|     if (!this.wss) { |     if (!this.webSocketServers.length) { | ||||||
|       throw new Error('WebSocket.Server is not set'); |       throw new Error('No WebSocket.Server have been set'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const blockTransactions = structuredClone(transactions); | ||||||
|  | 
 | ||||||
|     this.printLogs(); |     this.printLogs(); | ||||||
|     await statistics.runStatistics(); |     await statistics.runStatistics(); | ||||||
| 
 | 
 | ||||||
| @ -837,7 +944,7 @@ class WebsocketHandler { | |||||||
|     let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); |     let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); | ||||||
| 
 | 
 | ||||||
|     const accelerations = Object.values(mempool.getAccelerations()); |     const accelerations = Object.values(mempool.getAccelerations()); | ||||||
|     await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions); |     await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); | ||||||
| 
 | 
 | ||||||
|     const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); |     const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); | ||||||
|     memPool.handleMinedRbfTransactions(rbfTransactions); |     memPool.handleMinedRbfTransactions(rbfTransactions); | ||||||
| @ -846,22 +953,18 @@ class WebsocketHandler { | |||||||
|     if (config.MEMPOOL.AUDIT && memPool.isInSync()) { |     if (config.MEMPOOL.AUDIT && memPool.isInSync()) { | ||||||
|       let projectedBlocks; |       let projectedBlocks; | ||||||
|       const auditMempool = _memPool; |       const auditMempool = _memPool; | ||||||
|       const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); |       const isAccelerated = accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations())); | ||||||
| 
 | 
 | ||||||
|       if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) { |       if (config.MEMPOOL.RUST_GBT) { | ||||||
|         if (config.MEMPOOL.RUST_GBT) { |         const added = memPool.limitGBT ? (candidates?.added || []) : []; | ||||||
|           const added = memPool.limitGBT ? (candidates?.added || []) : []; |         const removed = memPool.limitGBT ? (candidates?.removed || []) : []; | ||||||
|           const removed = memPool.limitGBT ? (candidates?.removed || []) : []; |         projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); | ||||||
|           projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, auditMempool, added, removed, candidates, isAccelerated, block.extras.pool.id); |  | ||||||
|         } else { |  | ||||||
|           projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); |  | ||||||
|         } |  | ||||||
|       } else { |       } else { | ||||||
|         projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); |         projectedBlocks = await mempoolBlocks.$makeBlockTemplates(transactionIds, auditMempool, candidates, false, isAccelerated, block.extras.pool.id); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (Common.indexingEnabled()) { |       if (Common.indexingEnabled()) { | ||||||
|         const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); |         const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool); | ||||||
|         const matchRate = Math.round(score * 100 * 100) / 100; |         const matchRate = Math.round(score * 100 * 100) / 100; | ||||||
| 
 | 
 | ||||||
|         const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; |         const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : []; | ||||||
| @ -883,9 +986,11 @@ class WebsocketHandler { | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         BlocksAuditsRepository.$saveAudit({ |         BlocksAuditsRepository.$saveAudit({ | ||||||
|  |           version: 1, | ||||||
|           time: block.timestamp, |           time: block.timestamp, | ||||||
|           height: block.height, |           height: block.height, | ||||||
|           hash: block.id, |           hash: block.id, | ||||||
|  |           unseenTxs: unseen, | ||||||
|           addedTxs: added, |           addedTxs: added, | ||||||
|           prioritizedTxs: prioritized, |           prioritizedTxs: prioritized, | ||||||
|           missingTxs: censored, |           missingTxs: censored, | ||||||
| @ -937,7 +1042,7 @@ class WebsocketHandler { | |||||||
|       const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions; |       const removed = memPool.limitGBT ? (candidates?.removed || []) : transactions; | ||||||
|       await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true); |       await mempoolBlocks.$rustUpdateBlockTemplates(transactionIds, _memPool, added, removed, candidates, true); | ||||||
|     } else { |     } else { | ||||||
|       await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, config.MEMPOOL_SERVICES.ACCELERATIONS); |       await mempoolBlocks.$makeBlockTemplates(transactionIds, _memPool, candidates, true, true); | ||||||
|     } |     } | ||||||
|     const mBlocks = mempoolBlocks.getMempoolBlocks(); |     const mBlocks = mempoolBlocks.getMempoolBlocks(); | ||||||
|     const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); |     const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); | ||||||
| @ -961,6 +1066,31 @@ class WebsocketHandler { | |||||||
| 
 | 
 | ||||||
|     const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); |     const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); | ||||||
| 
 | 
 | ||||||
|  |     if (memPool.isInSync()) { | ||||||
|  |       this.mempoolSequence++; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; | ||||||
|  |     for (const txid of Object.keys(rbfTransactions)) { | ||||||
|  |       for (const replaced of rbfTransactions[txid].replaced) { | ||||||
|  |         replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const mempoolDeltaTxids: MempoolDeltaTxids = { | ||||||
|  |       sequence: this.mempoolSequence, | ||||||
|  |       added: [], | ||||||
|  |       removed: [], | ||||||
|  |       mined: transactions.map(tx => tx.txid), | ||||||
|  |       replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), | ||||||
|  |     }; | ||||||
|  |     const mempoolDelta: MempoolDelta = { | ||||||
|  |       sequence: this.mempoolSequence, | ||||||
|  |       added: [], | ||||||
|  |       removed: [], | ||||||
|  |       mined: transactions.map(tx => tx.txid), | ||||||
|  |       replaced: replacedTransactions, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     const responseCache = { ...this.socketData }; |     const responseCache = { ...this.socketData }; | ||||||
|     function getCachedResponse(key, data): string { |     function getCachedResponse(key, data): string { | ||||||
|       if (!responseCache[key]) { |       if (!responseCache[key]) { | ||||||
| @ -969,7 +1099,9 @@ class WebsocketHandler { | |||||||
|       return responseCache[key]; |       return responseCache[key]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.wss.clients.forEach((client) => { |     // TODO - Fix indentation after PR is merged
 | ||||||
|  |     for (const server of this.webSocketServers) { | ||||||
|  |     server.clients.forEach((client) => { | ||||||
|       if (client.readyState !== WebSocket.OPEN) { |       if (client.readyState !== WebSocket.OPEN) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
| @ -1010,7 +1142,11 @@ class WebsocketHandler { | |||||||
|               position: { |               position: { | ||||||
|                 ...mempoolTx.position, |                 ...mempoolTx.position, | ||||||
|                 accelerated: mempoolTx.acceleration || undefined, |                 accelerated: mempoolTx.acceleration || undefined, | ||||||
|               } |                 acceleratedBy: mempoolTx.acceleratedBy || undefined, | ||||||
|  |                 acceleratedAt: mempoolTx.acceleratedAt || undefined, | ||||||
|  |                 feeDelta: mempoolTx.feeDelta || undefined, | ||||||
|  |               }, | ||||||
|  |               accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid), | ||||||
|             }); |             }); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| @ -1029,6 +1165,9 @@ class WebsocketHandler { | |||||||
|                   ...mempoolTx.position, |                   ...mempoolTx.position, | ||||||
|                 }, |                 }, | ||||||
|                 accelerated: mempoolTx.acceleration || undefined, |                 accelerated: mempoolTx.acceleration || undefined, | ||||||
|  |                 acceleratedBy: mempoolTx.acceleratedBy || undefined, | ||||||
|  |                 acceleratedAt: mempoolTx.acceleratedAt || undefined, | ||||||
|  |                 feeDelta: mempoolTx.feeDelta || undefined, | ||||||
|               }; |               }; | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| @ -1135,21 +1274,32 @@ class WebsocketHandler { | |||||||
|           if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) { |           if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) { | ||||||
|             response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, { |             response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-full-${index}`, { | ||||||
|               index: index, |               index: index, | ||||||
|  |               sequence: this.mempoolSequence, | ||||||
|               blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx), |               blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx), | ||||||
|             }); |             }); | ||||||
|           } else { |           } else { | ||||||
|             response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, { |             response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-delta-${index}`, { | ||||||
|               index: index, |               index: index, | ||||||
|  |               sequence: this.mempoolSequence, | ||||||
|               delta: mBlockDeltas[index], |               delta: mBlockDeltas[index], | ||||||
|             }); |             }); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       if (client['track-mempool-txids']) { | ||||||
|  |         response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (client['track-mempool']) { | ||||||
|  |         response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (Object.keys(response).length) { |       if (Object.keys(response).length) { | ||||||
|         client.send(this.serializeResponse(response)); |         client.send(this.serializeResponse(response)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     await statistics.runStatistics(); |     await statistics.runStatistics(); | ||||||
|   } |   } | ||||||
| @ -1158,7 +1308,7 @@ class WebsocketHandler { | |||||||
|   // and zips it together into a valid JSON object
 |   // and zips it together into a valid JSON object
 | ||||||
|   private serializeResponse(response): string { |   private serializeResponse(response): string { | ||||||
|     return '{' |     return '{' | ||||||
|         + Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ') |         + Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ') | ||||||
|         + '}'; |         + '}'; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -1231,13 +1381,15 @@ class WebsocketHandler { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private printLogs(): void { |   private printLogs(): void { | ||||||
|     if (this.wss) { |     if (this.webSocketServers.length) { | ||||||
|       let numTxSubs = 0; |       let numTxSubs = 0; | ||||||
|       let numTxsSubs = 0; |       let numTxsSubs = 0; | ||||||
|       let numProjectedSubs = 0; |       let numProjectedSubs = 0; | ||||||
|       let numRbfSubs = 0; |       let numRbfSubs = 0; | ||||||
| 
 | 
 | ||||||
|       this.wss.clients.forEach((client) => { |       // TODO - Fix indentation after PR is merged
 | ||||||
|  |       for (const server of this.webSocketServers) { | ||||||
|  |       server.clients.forEach((client) => { | ||||||
|         if (client['track-tx']) { |         if (client['track-tx']) { | ||||||
|           numTxSubs++; |           numTxSubs++; | ||||||
|         } |         } | ||||||
| @ -1251,8 +1403,12 @@ class WebsocketHandler { | |||||||
|           numRbfSubs++; |           numRbfSubs++; | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       const count = this.wss?.clients?.size || 0; |       let count = 0; | ||||||
|  |       for (const server of this.webSocketServers) { | ||||||
|  |         count += server.clients?.size || 0; | ||||||
|  |       } | ||||||
|       const diff = count - this.numClients; |       const diff = count - this.numClients; | ||||||
|       this.numClients = count; |       this.numClients = count; | ||||||
|       logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`); |       logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`); | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ interface IConfig { | |||||||
|     NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet'; |     NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet'; | ||||||
|     BACKEND: 'esplora' | 'electrum' | 'none'; |     BACKEND: 'esplora' | 'electrum' | 'none'; | ||||||
|     HTTP_PORT: number; |     HTTP_PORT: number; | ||||||
|  |     UNIX_SOCKET_PATH: string; | ||||||
|     SPAWN_CLUSTER_PROCS: number; |     SPAWN_CLUSTER_PROCS: number; | ||||||
|     API_URL_PREFIX: string; |     API_URL_PREFIX: string; | ||||||
|     POLL_RATE_MS: number; |     POLL_RATE_MS: number; | ||||||
| @ -28,7 +29,7 @@ interface IConfig { | |||||||
|     EXTERNAL_RETRY_INTERVAL: number; |     EXTERNAL_RETRY_INTERVAL: number; | ||||||
|     USER_AGENT: string; |     USER_AGENT: string; | ||||||
|     STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; |     STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; | ||||||
|     AUTOMATIC_BLOCK_REINDEXING: boolean; |     AUTOMATIC_POOLS_UPDATE: boolean; | ||||||
|     POOLS_JSON_URL: string, |     POOLS_JSON_URL: string, | ||||||
|     POOLS_JSON_TREE_URL: string, |     POOLS_JSON_TREE_URL: string, | ||||||
|     AUDIT: boolean; |     AUDIT: boolean; | ||||||
| @ -50,6 +51,7 @@ interface IConfig { | |||||||
|     REQUEST_TIMEOUT: number; |     REQUEST_TIMEOUT: number; | ||||||
|     FALLBACK_TIMEOUT: number; |     FALLBACK_TIMEOUT: number; | ||||||
|     FALLBACK: string[]; |     FALLBACK: string[]; | ||||||
|  |     MAX_BEHIND_TIP: number; | ||||||
|   }; |   }; | ||||||
|   LIGHTNING: { |   LIGHTNING: { | ||||||
|     ENABLED: boolean; |     ENABLED: boolean; | ||||||
| @ -140,6 +142,8 @@ interface IConfig { | |||||||
|     ENABLED: boolean; |     ENABLED: boolean; | ||||||
|     AUDIT: boolean; |     AUDIT: boolean; | ||||||
|     AUDIT_START_HEIGHT: number; |     AUDIT_START_HEIGHT: number; | ||||||
|  |     STATISTICS: boolean; | ||||||
|  |     STATISTICS_START_TIME: number | string; | ||||||
|     SERVERS: string[]; |     SERVERS: string[]; | ||||||
|   }, |   }, | ||||||
|   MEMPOOL_SERVICES: { |   MEMPOOL_SERVICES: { | ||||||
| @ -153,6 +157,7 @@ interface IConfig { | |||||||
|   }, |   }, | ||||||
|   FIAT_PRICE: { |   FIAT_PRICE: { | ||||||
|     ENABLED: boolean; |     ENABLED: boolean; | ||||||
|  |     PAID: boolean; | ||||||
|     API_KEY: string; |     API_KEY: string; | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
| @ -164,6 +169,7 @@ const defaults: IConfig = { | |||||||
|     'NETWORK': 'mainnet', |     'NETWORK': 'mainnet', | ||||||
|     'BACKEND': 'none', |     'BACKEND': 'none', | ||||||
|     'HTTP_PORT': 8999, |     'HTTP_PORT': 8999, | ||||||
|  |     'UNIX_SOCKET_PATH': '', | ||||||
|     'SPAWN_CLUSTER_PROCS': 0, |     'SPAWN_CLUSTER_PROCS': 0, | ||||||
|     'API_URL_PREFIX': '/api/v1/', |     'API_URL_PREFIX': '/api/v1/', | ||||||
|     'POLL_RATE_MS': 2000, |     'POLL_RATE_MS': 2000, | ||||||
| @ -183,7 +189,7 @@ const defaults: IConfig = { | |||||||
|     'EXTERNAL_RETRY_INTERVAL': 0, |     'EXTERNAL_RETRY_INTERVAL': 0, | ||||||
|     'USER_AGENT': 'mempool', |     'USER_AGENT': 'mempool', | ||||||
|     'STDOUT_LOG_MIN_PRIORITY': 'debug', |     'STDOUT_LOG_MIN_PRIORITY': 'debug', | ||||||
|     'AUTOMATIC_BLOCK_REINDEXING': false, |     'AUTOMATIC_POOLS_UPDATE': false, | ||||||
|     'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', |     'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', | ||||||
|     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', |     'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', | ||||||
|     'AUDIT': false, |     'AUDIT': false, | ||||||
| @ -205,6 +211,7 @@ const defaults: IConfig = { | |||||||
|     'REQUEST_TIMEOUT': 10000, |     'REQUEST_TIMEOUT': 10000, | ||||||
|     'FALLBACK_TIMEOUT': 5000, |     'FALLBACK_TIMEOUT': 5000, | ||||||
|     'FALLBACK': [], |     'FALLBACK': [], | ||||||
|  |     'MAX_BEHIND_TIP': 2, | ||||||
|   }, |   }, | ||||||
|   'ELECTRUM': { |   'ELECTRUM': { | ||||||
|     'HOST': '127.0.0.1', |     'HOST': '127.0.0.1', | ||||||
| @ -295,6 +302,8 @@ const defaults: IConfig = { | |||||||
|     'ENABLED': false, |     'ENABLED': false, | ||||||
|     'AUDIT': false, |     'AUDIT': false, | ||||||
|     'AUDIT_START_HEIGHT': 774000, |     'AUDIT_START_HEIGHT': 774000, | ||||||
|  |     'STATISTICS': false, | ||||||
|  |     'STATISTICS_START_TIME': 1481932800, | ||||||
|     'SERVERS': [], |     'SERVERS': [], | ||||||
|   }, |   }, | ||||||
|   'MEMPOOL_SERVICES': { |   'MEMPOOL_SERVICES': { | ||||||
| @ -308,6 +317,7 @@ const defaults: IConfig = { | |||||||
|   }, |   }, | ||||||
|   'FIAT_PRICE': { |   'FIAT_PRICE': { | ||||||
|     'ENABLED': true, |     'ENABLED': true, | ||||||
|  |     'PAID': false, | ||||||
|     'API_KEY': '', |     'API_KEY': '', | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -2,8 +2,7 @@ import * as fs from 'fs'; | |||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import config from './config'; | import config from './config'; | ||||||
| import { createPool, Pool, PoolConnection } from 'mysql2/promise'; | import { createPool, Pool, PoolConnection } from 'mysql2/promise'; | ||||||
| import { LogLevel } from './logger'; | import logger, { LogLevel } from './logger'; | ||||||
| import logger from './logger'; |  | ||||||
| import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql'; | import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql'; | ||||||
| import { execSync } from 'child_process'; | import { execSync } from 'child_process'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -45,10 +45,13 @@ import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes'; | |||||||
| import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client'; | import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client'; | ||||||
| import accelerationRoutes from './api/acceleration/acceleration.routes'; | import accelerationRoutes from './api/acceleration/acceleration.routes'; | ||||||
| import aboutRoutes from './api/about.routes'; | import aboutRoutes from './api/about.routes'; | ||||||
|  | import mempoolBlocks from './api/mempool-blocks'; | ||||||
| 
 | 
 | ||||||
| class Server { | class Server { | ||||||
|   private wss: WebSocket.Server | undefined; |   private wss: WebSocket.Server | undefined; | ||||||
|  |   private wssUnixSocket: WebSocket.Server | undefined; | ||||||
|   private server: http.Server | undefined; |   private server: http.Server | undefined; | ||||||
|  |   private serverUnixSocket: http.Server | undefined; | ||||||
|   private app: Application; |   private app: Application; | ||||||
|   private currentBackendRetryInterval = 1; |   private currentBackendRetryInterval = 1; | ||||||
|   private backendRetryCount = 0; |   private backendRetryCount = 0; | ||||||
| @ -129,6 +132,7 @@ class Server { | |||||||
|       }) |       }) | ||||||
|       .use(express.urlencoded({ extended: true })) |       .use(express.urlencoded({ extended: true })) | ||||||
|       .use(express.text({ type: ['text/plain', 'application/base64'] })) |       .use(express.text({ type: ['text/plain', 'application/base64'] })) | ||||||
|  |       .use(express.json()) | ||||||
|       ; |       ; | ||||||
| 
 | 
 | ||||||
|     if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { |     if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { | ||||||
| @ -137,11 +141,16 @@ class Server { | |||||||
| 
 | 
 | ||||||
|     this.server = http.createServer(this.app); |     this.server = http.createServer(this.app); | ||||||
|     this.wss = new WebSocket.Server({ server: this.server }); |     this.wss = new WebSocket.Server({ server: this.server }); | ||||||
|  |     if (config.MEMPOOL.UNIX_SOCKET_PATH) { | ||||||
|  |       this.serverUnixSocket = http.createServer(this.app); | ||||||
|  |       this.wssUnixSocket = new WebSocket.Server({ server: this.serverUnixSocket }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     this.setUpWebsocketHandling(); |     this.setUpWebsocketHandling(); | ||||||
| 
 | 
 | ||||||
|     await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
 |     await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
 | ||||||
|     await syncAssets.syncAssets$(); |     await syncAssets.syncAssets$(); | ||||||
|  |     await mempoolBlocks.updatePools$(); | ||||||
|     if (config.MEMPOOL.ENABLED) { |     if (config.MEMPOOL.ENABLED) { | ||||||
|       if (config.MEMPOOL.CACHE_ENABLED) { |       if (config.MEMPOOL.CACHE_ENABLED) { | ||||||
|         await diskCache.$loadMempoolCache(); |         await diskCache.$loadMempoolCache(); | ||||||
| @ -192,6 +201,16 @@ class Server { | |||||||
|         logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); |         logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     if (this.serverUnixSocket) { | ||||||
|  |       this.serverUnixSocket.listen(config.MEMPOOL.UNIX_SOCKET_PATH, () => { | ||||||
|  |         if (worker) { | ||||||
|  |           logger.info(`Mempool Server worker #${process.pid} started`); | ||||||
|  |         } else { | ||||||
|  |           logger.notice(`Mempool Server is listening on ${config.MEMPOOL.UNIX_SOCKET_PATH}`); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async runMainUpdateLoop(): Promise<void> { |   async runMainUpdateLoop(): Promise<void> { | ||||||
| @ -210,7 +229,7 @@ class Server { | |||||||
|       const newMempool = await bitcoinApi.$getRawMempool(); |       const newMempool = await bitcoinApi.$getRawMempool(); | ||||||
|       const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; |       const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; | ||||||
|       const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; |       const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; | ||||||
|       const newAccelerations = await accelerationApi.$fetchAccelerations(); |       const newAccelerations = await accelerationApi.$updateAccelerations(); | ||||||
|       const numHandledBlocks = await blocks.$updateBlocks(); |       const numHandledBlocks = await blocks.$updateBlocks(); | ||||||
|       const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); |       const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); | ||||||
|       if (numHandledBlocks === 0) { |       if (numHandledBlocks === 0) { | ||||||
| @ -265,8 +284,12 @@ class Server { | |||||||
| 
 | 
 | ||||||
|   setUpWebsocketHandling(): void { |   setUpWebsocketHandling(): void { | ||||||
|     if (this.wss) { |     if (this.wss) { | ||||||
|       websocketHandler.setWebsocketServer(this.wss); |       websocketHandler.addWebsocketServer(this.wss); | ||||||
|     } |     } | ||||||
|  |     if (this.wssUnixSocket) { | ||||||
|  |       websocketHandler.addWebsocketServer(this.wssUnixSocket); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (Common.isLiquid() && config.DATABASE.ENABLED) { |     if (Common.isLiquid() && config.DATABASE.ENABLED) { | ||||||
|       blocks.setNewBlockCallback(async () => { |       blocks.setNewBlockCallback(async () => { | ||||||
|         try { |         try { | ||||||
| @ -310,7 +333,9 @@ class Server { | |||||||
|     if (config.MEMPOOL_SERVICES.ACCELERATIONS) { |     if (config.MEMPOOL_SERVICES.ACCELERATIONS) { | ||||||
|       accelerationRoutes.initRoutes(this.app); |       accelerationRoutes.initRoutes(this.app); | ||||||
|     } |     } | ||||||
|     aboutRoutes.initRoutes(this.app); |     if (!config.MEMPOOL.OFFICIAL) { | ||||||
|  |       aboutRoutes.initRoutes(this.app); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   healthCheck(): void { |   healthCheck(): void { | ||||||
| @ -338,6 +363,12 @@ class Server { | |||||||
|     if (config.DATABASE.ENABLED) { |     if (config.DATABASE.ENABLED) { | ||||||
|       DB.releasePidLock(); |       DB.releasePidLock(); | ||||||
|     } |     } | ||||||
|  |     this.server?.close(); | ||||||
|  |     this.serverUnixSocket?.close(); | ||||||
|  |     this.wss?.close(); | ||||||
|  |     if (this.wssUnixSocket) { | ||||||
|  |       this.wssUnixSocket.close(); | ||||||
|  |     } | ||||||
|     process.exit(code); |     process.exit(code); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,7 +8,9 @@ import priceUpdater from './tasks/price-updater'; | |||||||
| import PricesRepository from './repositories/PricesRepository'; | import PricesRepository from './repositories/PricesRepository'; | ||||||
| import config from './config'; | import config from './config'; | ||||||
| import auditReplicator from './replication/AuditReplication'; | import auditReplicator from './replication/AuditReplication'; | ||||||
|  | import statisticsReplicator from './replication/StatisticsReplication'; | ||||||
| import AccelerationRepository from './repositories/AccelerationRepository'; | import AccelerationRepository from './repositories/AccelerationRepository'; | ||||||
|  | import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; | ||||||
| 
 | 
 | ||||||
| export interface CoreIndex { | export interface CoreIndex { | ||||||
|   name: string; |   name: string; | ||||||
| @ -181,6 +183,7 @@ class Indexer { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.runSingleTask('blocksPrices'); |       this.runSingleTask('blocksPrices'); | ||||||
|  |       await blocks.$indexCoinbaseAddresses(); | ||||||
|       await mining.$indexDifficultyAdjustments(); |       await mining.$indexDifficultyAdjustments(); | ||||||
|       await mining.$generateNetworkHashrateHistory(); |       await mining.$generateNetworkHashrateHistory(); | ||||||
|       await mining.$generatePoolHashrateHistory(); |       await mining.$generatePoolHashrateHistory(); | ||||||
| @ -188,7 +191,9 @@ class Indexer { | |||||||
|       await blocks.$generateCPFPDatabase(); |       await blocks.$generateCPFPDatabase(); | ||||||
|       await blocks.$generateAuditStats(); |       await blocks.$generateAuditStats(); | ||||||
|       await auditReplicator.$sync(); |       await auditReplicator.$sync(); | ||||||
|  |       await statisticsReplicator.$sync(); | ||||||
|       await AccelerationRepository.$indexPastAccelerations(); |       await AccelerationRepository.$indexPastAccelerations(); | ||||||
|  |       await BlocksAuditsRepository.$migrateAuditsV0toV1(); | ||||||
|       // do not wait for classify blocks to finish
 |       // do not wait for classify blocks to finish
 | ||||||
|       blocks.$classifyBlocks(); |       blocks.$classifyBlocks(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|  | |||||||
| @ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface BlockAudit { | export interface BlockAudit { | ||||||
|  |   version: number, | ||||||
|   time: number, |   time: number, | ||||||
|   height: number, |   height: number, | ||||||
|   hash: string, |   hash: string, | ||||||
|  |   unseenTxs: string[], | ||||||
|   missingTxs: string[], |   missingTxs: string[], | ||||||
|   freshTxs: string[], |   freshTxs: string[], | ||||||
|   sigopTxs: string[], |   sigopTxs: string[], | ||||||
| @ -42,6 +44,19 @@ export interface BlockAudit { | |||||||
|   matchRate: number, |   matchRate: number, | ||||||
|   expectedFees?: number, |   expectedFees?: number, | ||||||
|   expectedWeight?: number, |   expectedWeight?: number, | ||||||
|  |   template?: any[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface TransactionAudit { | ||||||
|  |   seen?: boolean; | ||||||
|  |   expected?: boolean; | ||||||
|  |   added?: boolean; | ||||||
|  |   prioritized?: boolean; | ||||||
|  |   delayed?: number; | ||||||
|  |   accelerated?: boolean; | ||||||
|  |   conflict?: boolean; | ||||||
|  |   coinbase?: boolean; | ||||||
|  |   firstSeen?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface AuditScore { | export interface AuditScore { | ||||||
| @ -71,6 +86,22 @@ export interface MempoolBlockDelta { | |||||||
|   changed: MempoolDeltaChange[]; |   changed: MempoolDeltaChange[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface MempoolDeltaTxids { | ||||||
|  |   sequence: number, | ||||||
|  |   added: string[]; | ||||||
|  |   removed: string[]; | ||||||
|  |   mined: string[]; | ||||||
|  |   replaced: { replaced: string, by: string }[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface MempoolDelta { | ||||||
|  |   sequence: number, | ||||||
|  |   added: MempoolTransactionExtended[]; | ||||||
|  |   removed: string[]; | ||||||
|  |   mined: string[]; | ||||||
|  |   replaced: { replaced: string, by: TransactionExtended }[]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| interface VinStrippedToScriptsig { | interface VinStrippedToScriptsig { | ||||||
|   scriptsig: string; |   scriptsig: string; | ||||||
| } | } | ||||||
| @ -95,6 +126,9 @@ export interface TransactionExtended extends IEsploraApi.Transaction { | |||||||
|     vsize: number, |     vsize: number, | ||||||
|   }; |   }; | ||||||
|   acceleration?: boolean; |   acceleration?: boolean; | ||||||
|  |   acceleratedBy?: number[]; | ||||||
|  |   acceleratedAt?: number; | ||||||
|  |   feeDelta?: number; | ||||||
|   replacement?: boolean; |   replacement?: boolean; | ||||||
|   uid?: number; |   uid?: number; | ||||||
|   flags?: number; |   flags?: number; | ||||||
| @ -192,6 +226,7 @@ export interface CpfpInfo { | |||||||
|   sigops?: number; |   sigops?: number; | ||||||
|   adjustedVsize?: number, |   adjustedVsize?: number, | ||||||
|   acceleration?: boolean, |   acceleration?: boolean, | ||||||
|  |   fee?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface TransactionStripped { | export interface TransactionStripped { | ||||||
| @ -270,6 +305,7 @@ export interface BlockExtension { | |||||||
|   coinbaseRaw: string; |   coinbaseRaw: string; | ||||||
|   orphans: OrphanedBlock[] | null; |   orphans: OrphanedBlock[] | null; | ||||||
|   coinbaseAddress: string | null; |   coinbaseAddress: string | null; | ||||||
|  |   coinbaseAddresses: string[] | null; | ||||||
|   coinbaseSignature: string | null; |   coinbaseSignature: string | null; | ||||||
|   coinbaseSignatureAscii: string | null; |   coinbaseSignatureAscii: string | null; | ||||||
|   virtualSize: number; |   virtualSize: number; | ||||||
| @ -349,8 +385,9 @@ export interface CpfpCluster { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface CpfpSummary { | export interface CpfpSummary { | ||||||
|   transactions: TransactionExtended[]; |   transactions: MempoolTransactionExtended[]; | ||||||
|   clusters: CpfpCluster[]; |   clusters: CpfpCluster[]; | ||||||
|  |   version: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface Statistic { | export interface Statistic { | ||||||
| @ -406,6 +443,7 @@ export interface Statistic { | |||||||
| 
 | 
 | ||||||
| export interface OptimizedStatistic { | export interface OptimizedStatistic { | ||||||
|   added: string; |   added: string; | ||||||
|  |   count: number; | ||||||
|   vbytes_per_second: number; |   vbytes_per_second: number; | ||||||
|   total_fee: number; |   total_fee: number; | ||||||
|   mempool_byte_weight: number; |   mempool_byte_weight: number; | ||||||
| @ -415,7 +453,7 @@ export interface OptimizedStatistic { | |||||||
| 
 | 
 | ||||||
| export interface TxTrackingInfo { | export interface TxTrackingInfo { | ||||||
|   replacedBy?: string, |   replacedBy?: string, | ||||||
|   position?: { block: number, vsize: number, accelerated?: boolean }, |   position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number }, | ||||||
|   cpfp?: { |   cpfp?: { | ||||||
|     ancestors?: Ancestor[], |     ancestors?: Ancestor[], | ||||||
|     bestDescendant?: Ancestor | null, |     bestDescendant?: Ancestor | null, | ||||||
| @ -426,6 +464,9 @@ export interface TxTrackingInfo { | |||||||
|   }, |   }, | ||||||
|   utxoSpent?: { [vout: number]: { vin: number, txid: string } }, |   utxoSpent?: { [vout: number]: { vin: number, txid: string } }, | ||||||
|   accelerated?: boolean, |   accelerated?: boolean, | ||||||
|  |   acceleratedBy?: number[], | ||||||
|  |   acceleratedAt?: number, | ||||||
|  |   feeDelta?: number, | ||||||
|   confirmed?: boolean |   confirmed?: boolean | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -31,11 +31,11 @@ class AuditReplication { | |||||||
|     const missingAudits = await this.$getMissingAuditBlocks(); |     const missingAudits = await this.$getMissingAuditBlocks(); | ||||||
| 
 | 
 | ||||||
|     logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication'); |     logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication'); | ||||||
|      | 
 | ||||||
|     let totalSynced = 0; |     let totalSynced = 0; | ||||||
|     let totalMissed = 0; |     let totalMissed = 0; | ||||||
|     let loggerTimer = Date.now(); |     let loggerTimer = Date.now(); | ||||||
|     // process missing audits in batches of 
 |     // process missing audits in batches of BATCH_SIZE
 | ||||||
|     for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) { |     for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) { | ||||||
|       const slice = missingAudits.slice(i, i + BATCH_SIZE); |       const slice = missingAudits.slice(i, i + BATCH_SIZE); | ||||||
|       const results = await Promise.all(slice.map(hash => this.$syncAudit(hash))); |       const results = await Promise.all(slice.map(hash => this.$syncAudit(hash))); | ||||||
| @ -109,9 +109,11 @@ class AuditReplication { | |||||||
|       version: 1, |       version: 1, | ||||||
|     }); |     }); | ||||||
|     await blocksAuditsRepository.$saveAudit({ |     await blocksAuditsRepository.$saveAudit({ | ||||||
|  |       version: auditSummary.version || 0, | ||||||
|       hash: blockHash, |       hash: blockHash, | ||||||
|       height: auditSummary.height, |       height: auditSummary.height, | ||||||
|       time: auditSummary.timestamp || auditSummary.time, |       time: auditSummary.timestamp || auditSummary.time, | ||||||
|  |       unseenTxs: auditSummary.unseenTxs || [], | ||||||
|       missingTxs: auditSummary.missingTxs || [], |       missingTxs: auditSummary.missingTxs || [], | ||||||
|       addedTxs: auditSummary.addedTxs || [], |       addedTxs: auditSummary.addedTxs || [], | ||||||
|       prioritizedTxs: auditSummary.prioritizedTxs || [], |       prioritizedTxs: auditSummary.prioritizedTxs || [], | ||||||
|  | |||||||
							
								
								
									
										237
									
								
								backend/src/replication/StatisticsReplication.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								backend/src/replication/StatisticsReplication.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,237 @@ | |||||||
|  | import DB from '../database'; | ||||||
|  | import logger from '../logger'; | ||||||
|  | import { $sync } from './replicator'; | ||||||
|  | import config from '../config'; | ||||||
|  | import { Common } from '../api/common'; | ||||||
|  | import statistics from '../api/statistics/statistics-api'; | ||||||
|  | 
 | ||||||
|  | interface MissingStatistics { | ||||||
|  |   '24h': Set<number>; | ||||||
|  |   '1w': Set<number>; | ||||||
|  |   '1m': Set<number>; | ||||||
|  |   '3m': Set<number>; | ||||||
|  |   '6m': Set<number>; | ||||||
|  |   '2y': Set<number>; | ||||||
|  |   'all': Set<number>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const steps = { | ||||||
|  |   '24h': 60, | ||||||
|  |   '1w': 300, | ||||||
|  |   '1m': 1800, | ||||||
|  |   '3m': 7200, | ||||||
|  |   '6m': 10800, | ||||||
|  |   '2y': 28800, | ||||||
|  |   'all': 43200, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Syncs missing statistics data from trusted servers | ||||||
|  |  */ | ||||||
|  | class StatisticsReplication { | ||||||
|  |   inProgress: boolean = false; | ||||||
|  | 
 | ||||||
|  |   public async $sync(): Promise<void> { | ||||||
|  |     if (!config.REPLICATION.ENABLED || !config.REPLICATION.STATISTICS || !config.STATISTICS.ENABLED) { | ||||||
|  |       // replication not enabled, or statistics not enabled
 | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (this.inProgress) { | ||||||
|  |       logger.info(`StatisticsReplication sync already in progress`, 'Replication'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.inProgress = true; | ||||||
|  | 
 | ||||||
|  |     const missingStatistics = await this.$getMissingStatistics(); | ||||||
|  |     const missingIntervals = Object.keys(missingStatistics).filter(key => missingStatistics[key].size > 0); | ||||||
|  |     const totalMissing =  missingIntervals.reduce((total, key) => total + missingStatistics[key].size, 0); | ||||||
|  | 
 | ||||||
|  |     if (totalMissing === 0) { | ||||||
|  |       this.inProgress = false; | ||||||
|  |       logger.info(`Statistics table is complete, no replication needed`, 'Replication'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     for (const interval of missingIntervals) { | ||||||
|  |       logger.debug(`Missing ${missingStatistics[interval].size} statistics rows in '${interval}' timespan`, 'Replication'); | ||||||
|  |     } | ||||||
|  |     logger.debug(`Fetching ${missingIntervals.join(', ')} statistics endpoints from trusted servers to fill ${totalMissing} rows missing in statistics`, 'Replication'); | ||||||
|  |      | ||||||
|  |     let totalSynced = 0; | ||||||
|  |     let totalMissed = 0; | ||||||
|  | 
 | ||||||
|  |     for (const interval of missingIntervals) { | ||||||
|  |       const results = await this.$syncStatistics(interval, missingStatistics[interval]); | ||||||
|  |       totalSynced += results.synced; | ||||||
|  |       totalMissed += results.missed; | ||||||
|  | 
 | ||||||
|  |       logger.info(`Found ${totalSynced} / ${totalSynced + totalMissed} of ${totalMissing} missing statistics rows`, 'Replication'); | ||||||
|  |       await Common.sleep$(3000); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     logger.debug(`Synced ${totalSynced} statistics rows, ${totalMissed} still missing`, 'Replication'); | ||||||
|  | 
 | ||||||
|  |     this.inProgress = false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $syncStatistics(interval: string, missingTimes: Set<number>): Promise<any> { | ||||||
|  |    | ||||||
|  |     let success = false; | ||||||
|  |     let synced = 0; | ||||||
|  |     let missed = new Set(missingTimes); | ||||||
|  |     const syncResult = await $sync(`/api/v1/statistics/${interval}`); | ||||||
|  |     if (syncResult && syncResult.data?.length) { | ||||||
|  |       success = true; | ||||||
|  |       logger.info(`Fetched /api/v1/statistics/${interval} from ${syncResult.server}`);      | ||||||
|  |      | ||||||
|  |       for (const stat of syncResult.data) { | ||||||
|  |         const time = this.roundToNearestStep(stat.added, steps[interval]); | ||||||
|  |         if (missingTimes.has(time)) { | ||||||
|  |           try { | ||||||
|  |             await statistics.$create(statistics.mapOptimizedStatisticToStatistic([stat])[0], true); | ||||||
|  |             if (missed.delete(time)) { | ||||||
|  |               synced++; | ||||||
|  |             } | ||||||
|  |           } catch (e: any) { | ||||||
|  |             logger.err(`Failed to insert statistics row at ${stat.added} (${interval}) from ${syncResult.server}. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     } else { | ||||||
|  |       logger.warn(`An error occured when trying to fetch /api/v1/statistics/${interval}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { success, synced, missed: missed.size }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $getMissingStatistics(): Promise<MissingStatistics> { | ||||||
|  |     try { | ||||||
|  |       const now = Math.floor(Date.now() / 1000); | ||||||
|  |       const day = 60 * 60 * 24; | ||||||
|  | 
 | ||||||
|  |       const startTime = this.getStartTimeFromConfig(); | ||||||
|  | 
 | ||||||
|  |       const missingStatistics: MissingStatistics = { | ||||||
|  |         '24h': new Set<number>(), | ||||||
|  |         '1w': new Set<number>(), | ||||||
|  |         '1m': new Set<number>(), | ||||||
|  |         '3m': new Set<number>(), | ||||||
|  |         '6m': new Set<number>(), | ||||||
|  |         '2y': new Set<number>(), | ||||||
|  |         'all': new Set<number>() | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const intervals = [              // [start,               end,                 label ]
 | ||||||
|  |                                           [now - day + 600,     now - 60,            '24h']       , // from 24 hours ago to now = 1 minute granularity
 | ||||||
|  |         startTime < now - day ?           [now - day * 7,       now - day,           '1w' ] : null, // from 1 week ago to 24 hours ago = 5 minutes granularity
 | ||||||
|  |         startTime < now - day * 7 ?       [now - day * 30,      now - day * 7,       '1m' ] : null, // from 1 month ago to 1 week ago = 30 minutes granularity
 | ||||||
|  |         startTime < now - day * 30 ?      [now - day * 90,      now - day * 30,      '3m' ] : null, // from 3 months ago to 1 month ago = 2 hours granularity
 | ||||||
|  |         startTime < now - day * 90 ?      [now - day * 180,     now - day * 90,      '6m' ] : null, // from 6 months ago to 3 months ago = 3 hours granularity
 | ||||||
|  |         startTime < now - day * 180 ?     [now - day * 365 * 2, now - day * 180,     '2y' ] : null, // from 2 years ago to 6 months ago = 8 hours granularity
 | ||||||
|  |         startTime < now - day * 365 * 2 ? [startTime,           now - day * 365 * 2, 'all'] : null, // from start of statistics to 2 years ago = 12 hours granularity   
 | ||||||
|  |       ]; | ||||||
|  | 
 | ||||||
|  |       for (const interval of intervals) { | ||||||
|  |         if (!interval) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         missingStatistics[interval[2] as string] = await this.$getMissingStatisticsInterval(interval, startTime); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       return missingStatistics; | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async $getMissingStatisticsInterval(interval: any, startTime: number): Promise<Set<number>> { | ||||||
|  |     try { | ||||||
|  |       const start = interval[0]; | ||||||
|  |       const end = interval[1]; | ||||||
|  |       const step = steps[interval[2]]; | ||||||
|  | 
 | ||||||
|  |       const [rows]: any[] = await DB.query(` | ||||||
|  |         SELECT UNIX_TIMESTAMP(added) as added | ||||||
|  |         FROM statistics | ||||||
|  |         WHERE added >= FROM_UNIXTIME(?) AND added <= FROM_UNIXTIME(?) | ||||||
|  |         GROUP BY UNIX_TIMESTAMP(added) DIV ${step} ORDER BY statistics.added DESC | ||||||
|  |       `, [start, end]);
 | ||||||
|  | 
 | ||||||
|  |       const startingTime = Math.max(startTime, start) - Math.max(startTime, start) % step; | ||||||
|  | 
 | ||||||
|  |       const timeSteps: number[] = []; | ||||||
|  |       for (let time = startingTime; time < end; time += step) { | ||||||
|  |         timeSteps.push(time); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (timeSteps.length === 0) { | ||||||
|  |         return new Set<number>(); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const roundedTimesAlreadyHere: number[] = Array.from(new Set(rows.map(row => this.roundToNearestStep(row.added, step)))); | ||||||
|  | 
 | ||||||
|  |       const missingTimes = timeSteps.filter(time => !roundedTimesAlreadyHere.includes(time)).filter((time, i, arr) => { | ||||||
|  |         // Remove outsiders
 | ||||||
|  |         if (i === 0) { | ||||||
|  |           return arr[i + 1] === time + step | ||||||
|  |         } else if (i === arr.length - 1) { | ||||||
|  |           return arr[i - 1] === time - step; | ||||||
|  |         } | ||||||
|  |         return (arr[i + 1] === time + step) && (arr[i - 1] === time - step) | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       // Don't bother fetching if very few rows are missing
 | ||||||
|  |       if (missingTimes.length < timeSteps.length * 0.01) { | ||||||
|  |         return new Set(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return new Set(missingTimes); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot fetch missing statistics times from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private roundToNearestStep(time: number, step: number): number { | ||||||
|  |     const remainder = time % step; | ||||||
|  |     if (remainder < step / 2) { | ||||||
|  |       return time - remainder; | ||||||
|  |     } else { | ||||||
|  |       return time + (step - remainder); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getStartTimeFromConfig(): number { | ||||||
|  |     const now = Math.floor(Date.now() / 1000); | ||||||
|  |     const day = 60 * 60 * 24; | ||||||
|  | 
 | ||||||
|  |     let startTime: number; | ||||||
|  |     if (typeof(config.REPLICATION.STATISTICS_START_TIME) === 'string' && ['24h', '1w', '1m', '3m', '6m', '2y', 'all'].includes(config.REPLICATION.STATISTICS_START_TIME)) { | ||||||
|  |       if (config.REPLICATION.STATISTICS_START_TIME === 'all') { | ||||||
|  |         startTime = 1481932800; | ||||||
|  |       } else if (config.REPLICATION.STATISTICS_START_TIME === '2y') { | ||||||
|  |         startTime = now - day * 365 * 2; | ||||||
|  |       } else if (config.REPLICATION.STATISTICS_START_TIME === '6m') { | ||||||
|  |         startTime = now - day * 180; | ||||||
|  |       } else if (config.REPLICATION.STATISTICS_START_TIME === '3m') { | ||||||
|  |         startTime = now - day * 90; | ||||||
|  |       } else if (config.REPLICATION.STATISTICS_START_TIME === '1m') { | ||||||
|  |         startTime = now - day * 30; | ||||||
|  |       } else if (config.REPLICATION.STATISTICS_START_TIME === '1w') { | ||||||
|  |         startTime = now - day * 7; | ||||||
|  |       } else { | ||||||
|  |         startTime = now - day; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       startTime = Math.max(config.REPLICATION.STATISTICS_START_TIME as number || 1481932800, 1481932800); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return startTime; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new StatisticsReplication(); | ||||||
|  | 
 | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration'; | import { AccelerationInfo } from '../api/acceleration/acceleration'; | ||||||
| import { RowDataPacket } from 'mysql2'; | import { RowDataPacket } from 'mysql2'; | ||||||
| import DB from '../database'; | import DB from '../database'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| @ -6,15 +6,17 @@ import { IEsploraApi } from '../api/bitcoin/esplora-api.interface'; | |||||||
| import { Common } from '../api/common'; | import { Common } from '../api/common'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import blocks from '../api/blocks'; | import blocks from '../api/blocks'; | ||||||
| import accelerationApi, { Acceleration } from '../api/services/acceleration'; | import accelerationApi, { Acceleration, AccelerationHistory } from '../api/services/acceleration'; | ||||||
| import accelerationCosts from '../api/acceleration/acceleration'; | import accelerationCosts from '../api/acceleration/acceleration'; | ||||||
| import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; | import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; | ||||||
| import transactionUtils from '../api/transaction-utils'; | import transactionUtils from '../api/transaction-utils'; | ||||||
| import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; | import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; | ||||||
|  | import { makeBlockTemplate } from '../api/mini-miner'; | ||||||
| 
 | 
 | ||||||
| export interface PublicAcceleration { | export interface PublicAcceleration { | ||||||
|   txid: string, |   txid: string, | ||||||
|   height: number, |   height: number, | ||||||
|  |   added: number, | ||||||
|   pool: { |   pool: { | ||||||
|     id: number, |     id: number, | ||||||
|     slug: string, |     slug: string, | ||||||
| @ -29,15 +31,20 @@ export interface PublicAcceleration { | |||||||
| class AccelerationRepository { | class AccelerationRepository { | ||||||
|   private bidBoostV2Activated = 831580; |   private bidBoostV2Activated = 831580; | ||||||
| 
 | 
 | ||||||
|   public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number): Promise<void> { |   public async $saveAcceleration(acceleration: AccelerationInfo, block: IEsploraApi.Block, pool_id: number, accelerationData: Acceleration[]): Promise<void> { | ||||||
|  |     const accelerationMap: { [txid: string]: Acceleration } = {}; | ||||||
|  |     for (const acc of accelerationData) { | ||||||
|  |       accelerationMap[acc.txid] = acc; | ||||||
|  |     } | ||||||
|     try { |     try { | ||||||
|       await DB.query(` |       await DB.query(` | ||||||
|         INSERT INTO accelerations(txid, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost) |         INSERT INTO accelerations(txid, requested, added, height, pool, effective_vsize, effective_fee, boost_rate, boost_cost) | ||||||
|         VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?) |         VALUE (?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?) | ||||||
|         ON DUPLICATE KEY UPDATE |         ON DUPLICATE KEY UPDATE | ||||||
|           height = ? |           height = ? | ||||||
|       `, [
 |       `, [
 | ||||||
|         acceleration.txSummary.txid, |         acceleration.txSummary.txid, | ||||||
|  |         accelerationMap[acceleration.txSummary.txid].added, | ||||||
|         block.timestamp, |         block.timestamp, | ||||||
|         block.height, |         block.height, | ||||||
|         pool_id, |         pool_id, | ||||||
| @ -64,7 +71,7 @@ class AccelerationRepository { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let query = ` |     let query = ` | ||||||
|       SELECT * FROM accelerations |       SELECT *, UNIX_TIMESTAMP(requested) as requested_timestamp, UNIX_TIMESTAMP(added) as block_timestamp FROM accelerations | ||||||
|       JOIN pools on pools.unique_id = accelerations.pool |       JOIN pools on pools.unique_id = accelerations.pool | ||||||
|     `;
 |     `;
 | ||||||
|     let params: any[] = []; |     let params: any[] = []; | ||||||
| @ -99,6 +106,7 @@ class AccelerationRepository { | |||||||
|         return rows.map(row => ({ |         return rows.map(row => ({ | ||||||
|           txid: row.txid, |           txid: row.txid, | ||||||
|           height: row.height, |           height: row.height, | ||||||
|  |           added: row.requested_timestamp || row.block_timestamp, | ||||||
|           pool: { |           pool: { | ||||||
|             id: row.id, |             id: row.id, | ||||||
|             slug: row.slug, |             slug: row.slug, | ||||||
| @ -184,6 +192,7 @@ class AccelerationRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // modifies block transactions
 | ||||||
|   public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> { |   public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> { | ||||||
|     const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; |     const blockTxs: { [txid: string]: MempoolTransactionExtended } = {}; | ||||||
|     for (const tx of transactions) { |     for (const tx of transactions) { | ||||||
| @ -202,9 +211,18 @@ class AccelerationRepository { | |||||||
|         const tx = blockTxs[acc.txid]; |         const tx = blockTxs[acc.txid]; | ||||||
|         const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); |         const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); | ||||||
|         accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); |         accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); | ||||||
|         this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id); |         this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, successfulAccelerations); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     let anyConfirmed = false; | ||||||
|  |     for (const acc of accelerations) { | ||||||
|  |       if (blockTxs[acc.txid]) { | ||||||
|  |         anyConfirmed = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (anyConfirmed) { | ||||||
|  |       accelerationApi.accelerationConfirmed(); | ||||||
|  |     } | ||||||
|     const lastSyncedHeight = await this.$getLastSyncedHeight(); |     const lastSyncedHeight = await this.$getLastSyncedHeight(); | ||||||
|     // if we've missed any blocks, let the indexer catch up from the last synced height on the next run
 |     // if we've missed any blocks, let the indexer catch up from the last synced height on the next run
 | ||||||
|     if (block.height === lastSyncedHeight + 1) { |     if (block.height === lastSyncedHeight + 1) { | ||||||
| @ -230,13 +248,15 @@ class AccelerationRepository { | |||||||
|     logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`); |     logger.debug(`Fetching accelerations between block ${lastSyncedHeight} and ${currentHeight}`); | ||||||
| 
 | 
 | ||||||
|     // Fetch accelerations from mempool.space since the last synced block;
 |     // Fetch accelerations from mempool.space since the last synced block;
 | ||||||
|     const accelerationsByBlock = {}; |     const accelerationsByBlock: {[height: number]: AccelerationHistory[]} = {}; | ||||||
|     const blockHashes = {}; |     const blockHashes = {}; | ||||||
|     let done = false; |     let done = false; | ||||||
|     let page = 1; |     let page = 1; | ||||||
|     let count = 0; |     let count = 0; | ||||||
|     try { |     try { | ||||||
|       while (!done) { |       while (!done) { | ||||||
|  |         // don't DDoS the services backend
 | ||||||
|  |         Common.sleep$(500 + (Math.random() * 1000)); | ||||||
|         const accelerations = await accelerationApi.$fetchAccelerationHistory(page); |         const accelerations = await accelerationApi.$fetchAccelerationHistory(page); | ||||||
|         page++; |         page++; | ||||||
|         if (!accelerations?.length) { |         if (!accelerations?.length) { | ||||||
| @ -297,12 +317,16 @@ class AccelerationRepository { | |||||||
|           const feeStats = Common.calcEffectiveFeeStatistics(template); |           const feeStats = Common.calcEffectiveFeeStatistics(template); | ||||||
|           boostRate = feeStats.medianFee; |           boostRate = feeStats.medianFee; | ||||||
|         } |         } | ||||||
|  |         const accelerationSummaries = accelerations.map(acc => ({ | ||||||
|  |           ...acc, | ||||||
|  |           pools: acc.pools, | ||||||
|  |         })) | ||||||
|         for (const acc of accelerations) { |         for (const acc of accelerations) { | ||||||
|           if (blockTxs[acc.txid]) { |           if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) { | ||||||
|             const tx = blockTxs[acc.txid]; |             const tx = blockTxs[acc.txid]; | ||||||
|             const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); |             const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions); | ||||||
|             accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); |             accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost)); | ||||||
|             await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id); |             await this.$saveAcceleration(accelerationInfo, block, block.extras.pool.id, accelerationSummaries); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         await this.$setLastSyncedHeight(height); |         await this.$setLastSyncedHeight(height); | ||||||
| @ -317,6 +341,26 @@ class AccelerationRepository { | |||||||
| 
 | 
 | ||||||
|     logger.debug(`Indexing accelerations completed`); |     logger.debug(`Indexing accelerations completed`); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Delete accelerations from the database above blockHeight | ||||||
|  |    */ | ||||||
|  |   public async $deleteAccelerationsFrom(blockHeight: number): Promise<void> { | ||||||
|  |     logger.info(`Delete newer accelerations from height ${blockHeight} from the database`); | ||||||
|  |     try { | ||||||
|  |       const currentSyncedHeight = await this.$getLastSyncedHeight(); | ||||||
|  |       if (currentSyncedHeight >= blockHeight) { | ||||||
|  |         await DB.query(` | ||||||
|  |           UPDATE state | ||||||
|  |           SET number = ? | ||||||
|  |           WHERE name = 'last_acceleration_block' | ||||||
|  |         `, [blockHeight - 1]);
 | ||||||
|  |       } | ||||||
|  |       await DB.query(`DELETE FROM accelerations where height >= ${blockHeight}`); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err('Cannot delete indexed accelerations. Reason: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new AccelerationRepository(); | export default new AccelerationRepository(); | ||||||
|  | |||||||
| @ -1,13 +1,24 @@ | |||||||
| import blocks from '../api/blocks'; |  | ||||||
| import DB from '../database'; | import DB from '../database'; | ||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import { BlockAudit, AuditScore } from '../mempool.interfaces'; | import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; | ||||||
|  | import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces'; | ||||||
|  | 
 | ||||||
|  | interface MigrationAudit { | ||||||
|  |   version: number, | ||||||
|  |   height: number, | ||||||
|  |   id: string, | ||||||
|  |   timestamp: number, | ||||||
|  |   prioritizedTxs: string[], | ||||||
|  |   acceleratedTxs: string[], | ||||||
|  |   template: TransactionStripped[], | ||||||
|  |   transactions: TransactionStripped[], | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| class BlocksAuditRepositories { | class BlocksAuditRepositories { | ||||||
|   public async $saveAudit(audit: BlockAudit): Promise<void> { |   public async $saveAudit(audit: BlockAudit): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
 |       await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
 | ||||||
|         VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
 |         VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
 | ||||||
|           JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); |           JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]); | ||||||
|     } catch (e: any) { |     } catch (e: any) { | ||||||
|       if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 |       if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
 | ||||||
| @ -62,24 +73,30 @@ class BlocksAuditRepositories { | |||||||
|   public async $getBlockAudit(hash: string): Promise<BlockAudit | null> { |   public async $getBlockAudit(hash: string): Promise<BlockAudit | null> { | ||||||
|     try { |     try { | ||||||
|       const [rows]: any[] = await DB.query( |       const [rows]: any[] = await DB.query( | ||||||
|         `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
 |         `SELECT
 | ||||||
|         template, |           blocks_audits.version, | ||||||
|         missing_txs as missingTxs, |           blocks_audits.height, | ||||||
|         added_txs as addedTxs, |           blocks_audits.hash as id, | ||||||
|         prioritized_txs as prioritizedTxs, |           UNIX_TIMESTAMP(blocks_audits.time) as timestamp, | ||||||
|         fresh_txs as freshTxs, |           template, | ||||||
|         sigop_txs as sigopTxs, |           unseen_txs as unseenTxs, | ||||||
|         fullrbf_txs as fullrbfTxs, |           missing_txs as missingTxs, | ||||||
|         accelerated_txs as acceleratedTxs, |           added_txs as addedTxs, | ||||||
|         match_rate as matchRate, |           prioritized_txs as prioritizedTxs, | ||||||
|         expected_fees as expectedFees, |           fresh_txs as freshTxs, | ||||||
|         expected_weight as expectedWeight |           sigop_txs as sigopTxs, | ||||||
|  |           fullrbf_txs as fullrbfTxs, | ||||||
|  |           accelerated_txs as acceleratedTxs, | ||||||
|  |           match_rate as matchRate, | ||||||
|  |           expected_fees as expectedFees, | ||||||
|  |           expected_weight as expectedWeight | ||||||
|         FROM blocks_audits |         FROM blocks_audits | ||||||
|         JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash |         JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash | ||||||
|         WHERE blocks_audits.hash = ? |         WHERE blocks_audits.hash = ? | ||||||
|       `, [hash]);
 |       `, [hash]);
 | ||||||
|        |        | ||||||
|       if (rows.length) { |       if (rows.length) { | ||||||
|  |         rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs); | ||||||
|         rows[0].missingTxs = JSON.parse(rows[0].missingTxs); |         rows[0].missingTxs = JSON.parse(rows[0].missingTxs); | ||||||
|         rows[0].addedTxs = JSON.parse(rows[0].addedTxs); |         rows[0].addedTxs = JSON.parse(rows[0].addedTxs); | ||||||
|         rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs); |         rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs); | ||||||
| @ -98,6 +115,42 @@ class BlocksAuditRepositories { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> { | ||||||
|  |     try { | ||||||
|  |       const blockAudit = await this.$getBlockAudit(hash); | ||||||
|  | 
 | ||||||
|  |       if (blockAudit) { | ||||||
|  |         const isAdded = blockAudit.addedTxs.includes(txid); | ||||||
|  |         const isPrioritized = blockAudit.prioritizedTxs.includes(txid); | ||||||
|  |         const isAccelerated = blockAudit.acceleratedTxs.includes(txid); | ||||||
|  |         const isConflict = blockAudit.fullrbfTxs.includes(txid); | ||||||
|  |         let isExpected = false; | ||||||
|  |         let firstSeen = undefined; | ||||||
|  |         blockAudit.template?.forEach(tx => { | ||||||
|  |           if (tx.txid === txid) { | ||||||
|  |             isExpected = true; | ||||||
|  |             firstSeen = tx.time; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |         const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated); | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |           seen: wasSeen, | ||||||
|  |           expected: isExpected, | ||||||
|  |           added: isAdded && (blockAudit.version === 0 || !wasSeen), | ||||||
|  |           prioritized: isPrioritized, | ||||||
|  |           conflict: isConflict, | ||||||
|  |           accelerated: isAccelerated, | ||||||
|  |           firstSeen, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $getBlockAuditScore(hash: string): Promise<AuditScore> { |   public async $getBlockAuditScore(hash: string): Promise<AuditScore> { | ||||||
|     try { |     try { | ||||||
|       const [rows]: any[] = await DB.query( |       const [rows]: any[] = await DB.query( | ||||||
| @ -151,6 +204,96 @@ class BlocksAuditRepositories { | |||||||
|       throw e; |       throw e; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * [INDEXING] Migrate audits from v0 to v1 | ||||||
|  |    */ | ||||||
|  |   public async $migrateAuditsV0toV1(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       let done = false; | ||||||
|  |       let processed = 0; | ||||||
|  |       let lastHeight; | ||||||
|  |       while (!done) { | ||||||
|  |         const [toMigrate]: MigrationAudit[][] = await DB.query( | ||||||
|  |           `SELECT
 | ||||||
|  |             blocks_audits.height as height, | ||||||
|  |             blocks_audits.hash as id, | ||||||
|  |             UNIX_TIMESTAMP(blocks_audits.time) as timestamp, | ||||||
|  |             blocks_summaries.transactions as transactions, | ||||||
|  |             blocks_templates.template as template, | ||||||
|  |             blocks_audits.prioritized_txs as prioritizedTxs, | ||||||
|  |             blocks_audits.accelerated_txs as acceleratedTxs | ||||||
|  |           FROM blocks_audits | ||||||
|  |           JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash | ||||||
|  |           JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash | ||||||
|  |           WHERE blocks_audits.version = 0 | ||||||
|  |           AND blocks_summaries.version = 2 | ||||||
|  |           ORDER BY blocks_audits.height DESC | ||||||
|  |           LIMIT 100 | ||||||
|  |         `) as any[];
 | ||||||
|  | 
 | ||||||
|  |         if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) { | ||||||
|  |           done = true; | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         lastHeight = toMigrate[0].height; | ||||||
|  | 
 | ||||||
|  |         logger.info(`migrating ${toMigrate.length} audits to version 1`); | ||||||
|  | 
 | ||||||
|  |         for (const audit of toMigrate) { | ||||||
|  |           // unpack JSON-serialized transaction lists
 | ||||||
|  |           audit.transactions = JSON.parse((audit.transactions as any as string) || '[]'); | ||||||
|  |           audit.template = JSON.parse((audit.template as any as string) || '[]'); | ||||||
|  | 
 | ||||||
|  |           // we know transactions in the template, or marked "prioritized" or "accelerated"
 | ||||||
|  |           // were seen in our mempool before the block was mined.
 | ||||||
|  |           const isSeen = new Set<string>(); | ||||||
|  |           for (const tx of audit.template) { | ||||||
|  |             isSeen.add(tx.txid); | ||||||
|  |           } | ||||||
|  |           for (const txid of audit.prioritizedTxs) { | ||||||
|  |             isSeen.add(txid); | ||||||
|  |           } | ||||||
|  |           for (const txid of audit.acceleratedTxs) { | ||||||
|  |             isSeen.add(txid); | ||||||
|  |           } | ||||||
|  |           const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid)); | ||||||
|  | 
 | ||||||
|  |           // identify "prioritized" transactions
 | ||||||
|  |           const prioritizedTxs: string[] = []; | ||||||
|  |           let lastEffectiveRate = 0; | ||||||
|  |           // Iterate over the mined template from bottom to top (excluding the coinbase)
 | ||||||
|  |           // Transactions should appear in ascending order of mining priority.
 | ||||||
|  |           for (let i = audit.transactions.length - 1; i > 0; i--) { | ||||||
|  |             const blockTx = audit.transactions[i]; | ||||||
|  |             // If a tx has a lower in-band effective fee rate than the previous tx,
 | ||||||
|  |             // it must have been prioritized out-of-band (in order to have a higher mining priority)
 | ||||||
|  |             // so exclude from the analysis.
 | ||||||
|  |             if ((blockTx.rate || 0) < lastEffectiveRate) { | ||||||
|  |               prioritizedTxs.push(blockTx.txid); | ||||||
|  |             } else { | ||||||
|  |               lastEffectiveRate = blockTx.rate || 0; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           // Update audit in the database
 | ||||||
|  |           await DB.query(` | ||||||
|  |             UPDATE blocks_audits SET | ||||||
|  |               version = ?, | ||||||
|  |               unseen_txs = ?, | ||||||
|  |               prioritized_txs = ? | ||||||
|  |             WHERE hash = ? | ||||||
|  |           `, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         processed += toMigrate.length; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logger.info(`migrated ${processed} audits to version 1`); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new BlocksAuditRepositories(); | export default new BlocksAuditRepositories(); | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ import logger from '../logger'; | |||||||
| import { Common } from '../api/common'; | import { Common } from '../api/common'; | ||||||
| import PoolsRepository from './PoolsRepository'; | import PoolsRepository from './PoolsRepository'; | ||||||
| import HashratesRepository from './HashratesRepository'; | import HashratesRepository from './HashratesRepository'; | ||||||
| import { RowDataPacket, escape } from 'mysql2'; | import { RowDataPacket } from 'mysql2'; | ||||||
| import BlocksSummariesRepository from './BlocksSummariesRepository'; | import BlocksSummariesRepository from './BlocksSummariesRepository'; | ||||||
| import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; | import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; | ||||||
| import bitcoinClient from '../api/bitcoin/bitcoin-client'; | import bitcoinClient from '../api/bitcoin/bitcoin-client'; | ||||||
| @ -40,6 +40,7 @@ interface DatabaseBlock { | |||||||
|   avgFeeRate: number; |   avgFeeRate: number; | ||||||
|   coinbaseRaw: string; |   coinbaseRaw: string; | ||||||
|   coinbaseAddress: string; |   coinbaseAddress: string; | ||||||
|  |   coinbaseAddresses: string; | ||||||
|   coinbaseSignature: string; |   coinbaseSignature: string; | ||||||
|   coinbaseSignatureAscii: string; |   coinbaseSignatureAscii: string; | ||||||
|   avgTxSize: number; |   avgTxSize: number; | ||||||
| @ -82,6 +83,7 @@ const BLOCK_DB_FIELDS = ` | |||||||
|   blocks.avg_fee_rate AS avgFeeRate, |   blocks.avg_fee_rate AS avgFeeRate, | ||||||
|   blocks.coinbase_raw AS coinbaseRaw, |   blocks.coinbase_raw AS coinbaseRaw, | ||||||
|   blocks.coinbase_address AS coinbaseAddress, |   blocks.coinbase_address AS coinbaseAddress, | ||||||
|  |   blocks.coinbase_addresses AS coinbaseAddresses, | ||||||
|   blocks.coinbase_signature AS coinbaseSignature, |   blocks.coinbase_signature AS coinbaseSignature, | ||||||
|   blocks.coinbase_signature_ascii AS coinbaseSignatureAscii, |   blocks.coinbase_signature_ascii AS coinbaseSignatureAscii, | ||||||
|   blocks.avg_tx_size AS avgTxSize, |   blocks.avg_tx_size AS avgTxSize, | ||||||
| @ -114,7 +116,7 @@ class BlocksRepository { | |||||||
|         pool_id,            fees,                fee_span,          median_fee, |         pool_id,            fees,                fee_span,          median_fee, | ||||||
|         reward,             version,             bits,              nonce, |         reward,             version,             bits,              nonce, | ||||||
|         merkle_root,        previous_block_hash, avg_fee,           avg_fee_rate, |         merkle_root,        previous_block_hash, avg_fee,           avg_fee_rate, | ||||||
|         median_timestamp,   header,              coinbase_address, |         median_timestamp,   header,              coinbase_address,  coinbase_addresses, | ||||||
|         coinbase_signature, utxoset_size,        utxoset_change,    avg_tx_size, |         coinbase_signature, utxoset_size,        utxoset_change,    avg_tx_size, | ||||||
|         total_inputs,       total_outputs,       total_input_amt,   total_output_amt, |         total_inputs,       total_outputs,       total_input_amt,   total_output_amt, | ||||||
|         fee_percentiles,    segwit_total_txs,    segwit_total_size, segwit_total_weight, |         fee_percentiles,    segwit_total_txs,    segwit_total_size, segwit_total_weight, | ||||||
| @ -125,7 +127,7 @@ class BlocksRepository { | |||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
|         FROM_UNIXTIME(?), ?, ?, |         FROM_UNIXTIME(?), ?, ?, ?, | ||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
|         ?, ?, ?, ?, |         ?, ?, ?, ?, | ||||||
| @ -161,6 +163,7 @@ class BlocksRepository { | |||||||
|         block.mediantime, |         block.mediantime, | ||||||
|         block.extras.header, |         block.extras.header, | ||||||
|         block.extras.coinbaseAddress, |         block.extras.coinbaseAddress, | ||||||
|  |         block.extras.coinbaseAddresses ? JSON.stringify(block.extras.coinbaseAddresses) : null, | ||||||
|         truncatedCoinbaseSignature, |         truncatedCoinbaseSignature, | ||||||
|         block.extras.utxoSetSize, |         block.extras.utxoSetSize, | ||||||
|         block.extras.utxoSetChange, |         block.extras.utxoSetChange, | ||||||
| @ -529,7 +532,7 @@ class BlocksRepository { | |||||||
|         return null; |         return null; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);   |       return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e)); |       logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|       throw e; |       throw e; | ||||||
| @ -663,7 +666,7 @@ class BlocksRepository { | |||||||
|   /** |   /** | ||||||
|    * Get the historical averaged block fees |    * Get the historical averaged block fees | ||||||
|    */ |    */ | ||||||
|   public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> { |   public async $getHistoricalBlockFees(div: number, interval: string | null, timespan?: {from: number, to: number}): Promise<any> { | ||||||
|     try { |     try { | ||||||
|       let query = `SELECT
 |       let query = `SELECT
 | ||||||
|         CAST(AVG(blocks.height) as INT) as avgHeight, |         CAST(AVG(blocks.height) as INT) as avgHeight, | ||||||
| @ -677,6 +680,8 @@ class BlocksRepository { | |||||||
| 
 | 
 | ||||||
|       if (interval !== null) { |       if (interval !== null) { | ||||||
|         query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; |         query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; | ||||||
|  |       } else if (timespan) { | ||||||
|  |         query += ` WHERE blockTimestamp BETWEEN FROM_UNIXTIME(${timespan.from}) AND FROM_UNIXTIME(${timespan.to})`; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; |       query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; | ||||||
| @ -920,6 +925,25 @@ class BlocksRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Get all indexed blocks with missing coinbase addresses | ||||||
|  |    */ | ||||||
|  |   public async $getBlocksWithoutCoinbaseAddresses(): Promise<any> { | ||||||
|  |     try { | ||||||
|  |       const [blocks] = await DB.query(` | ||||||
|  |         SELECT height, hash, coinbase_addresses | ||||||
|  |         FROM blocks | ||||||
|  |         WHERE coinbase_addresses IS NULL AND | ||||||
|  |           coinbase_address IS NOT NULL | ||||||
|  |         ORDER BY height DESC | ||||||
|  |       `);
 | ||||||
|  |       return blocks; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot get blocks with missing coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Save indexed median fee to avoid recomputing it later |    * Save indexed median fee to avoid recomputing it later | ||||||
|    *  |    *  | ||||||
| @ -958,6 +982,44 @@ class BlocksRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Save coinbase addresses | ||||||
|  |    *  | ||||||
|  |    * @param id | ||||||
|  |    * @param addresses | ||||||
|  |    */ | ||||||
|  |   public async $saveCoinbaseAddresses(id: string, addresses: string[]): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       await DB.query(` | ||||||
|  |         UPDATE blocks SET coinbase_addresses = ? | ||||||
|  |         WHERE hash = ?`,
 | ||||||
|  |         [JSON.stringify(addresses), id] | ||||||
|  |       ); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot update block coinbase addresses. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Save pool | ||||||
|  |    *  | ||||||
|  |    * @param id | ||||||
|  |    * @param poolId | ||||||
|  |    */ | ||||||
|  |   public async $savePool(id: string, poolId: number): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       await DB.query(` | ||||||
|  |         UPDATE blocks SET pool_id = ? | ||||||
|  |         WHERE hash = ?`,
 | ||||||
|  |         [poolId, id] | ||||||
|  |       ); | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Convert a mysql row block into a BlockExtended. Note that you |    * Convert a mysql row block into a BlockExtended. Note that you | ||||||
|    * must provide the correct field into dbBlk object param |    * must provide the correct field into dbBlk object param | ||||||
| @ -997,6 +1059,7 @@ class BlocksRepository { | |||||||
|     extras.avgFeeRate = dbBlk.avgFeeRate; |     extras.avgFeeRate = dbBlk.avgFeeRate; | ||||||
|     extras.coinbaseRaw = dbBlk.coinbaseRaw; |     extras.coinbaseRaw = dbBlk.coinbaseRaw; | ||||||
|     extras.coinbaseAddress = dbBlk.coinbaseAddress; |     extras.coinbaseAddress = dbBlk.coinbaseAddress; | ||||||
|  |     extras.coinbaseAddresses = dbBlk.coinbaseAddresses ? JSON.parse(dbBlk.coinbaseAddresses) : []; | ||||||
|     extras.coinbaseSignature = dbBlk.coinbaseSignature; |     extras.coinbaseSignature = dbBlk.coinbaseSignature; | ||||||
|     extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii; |     extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii; | ||||||
|     extras.avgTxSize = dbBlk.avgTxSize; |     extras.avgTxSize = dbBlk.avgTxSize; | ||||||
| @ -1043,7 +1106,7 @@ class BlocksRepository { | |||||||
|         let summaryVersion = 0; |         let summaryVersion = 0; | ||||||
|         if (config.MEMPOOL.BACKEND === 'esplora') { |         if (config.MEMPOOL.BACKEND === 'esplora') { | ||||||
|           const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); |           const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); | ||||||
|           summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); |           summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs); | ||||||
|           summaryVersion = 1; |           summaryVersion = 1; | ||||||
|         } else { |         } else { | ||||||
|           // Call Core RPC
 |           // Call Core RPC
 | ||||||
|  | |||||||
| @ -114,6 +114,43 @@ class BlocksSummariesRepository { | |||||||
|     return []; |     return []; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getSummariesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { | ||||||
|  |     try { | ||||||
|  |       const [rows]: any[] = await DB.query(` | ||||||
|  |         SELECT | ||||||
|  |           height, | ||||||
|  |           id, | ||||||
|  |           version | ||||||
|  |         FROM blocks_summaries | ||||||
|  |         WHERE version < ? | ||||||
|  |         ORDER BY height DESC;`, [version]);
 | ||||||
|  |       return rows; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public async $getTemplatesBelowVersion(version: number): Promise<{ height: number, id: string, version: number }[]> { | ||||||
|  |     try { | ||||||
|  |       const [rows]: any[] = await DB.query(` | ||||||
|  |         SELECT | ||||||
|  |           blocks_summaries.height as height, | ||||||
|  |           blocks_templates.id as id, | ||||||
|  |           blocks_templates.version as version | ||||||
|  |         FROM blocks_templates | ||||||
|  |         JOIN blocks_summaries ON blocks_templates.id = blocks_summaries.id | ||||||
|  |         WHERE blocks_templates.version < ? | ||||||
|  |         ORDER BY height DESC;`, [version]);
 | ||||||
|  |       return rows; | ||||||
|  |     } catch (e) { | ||||||
|  |       logger.err(`Cannot get block summaries below version. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Get the fee percentiles if the block has already been indexed, [] otherwise |    * Get the fee percentiles if the block has already been indexed, [] otherwise | ||||||
|    *  |    *  | ||||||
|  | |||||||
| @ -91,6 +91,26 @@ class CpfpRepository { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $getClustersAt(height: number): Promise<CpfpCluster[]> { | ||||||
|  |     const [clusterRows]: any = await DB.query( | ||||||
|  |       ` | ||||||
|  |         SELECT * | ||||||
|  |         FROM compact_cpfp_clusters | ||||||
|  |         WHERE height = ? | ||||||
|  |       `,
 | ||||||
|  |       [height] | ||||||
|  |     ); | ||||||
|  |     return clusterRows.map(cluster => { | ||||||
|  |       if (cluster?.txs) { | ||||||
|  |         cluster.effectiveFeePerVsize = cluster.fee_rate; | ||||||
|  |         cluster.txs = this.unpack(cluster.txs); | ||||||
|  |         return cluster; | ||||||
|  |       } else { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |     }).filter(cluster => cluster !== null); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   public async $deleteClustersFrom(height: number): Promise<void> { |   public async $deleteClustersFrom(height: number): Promise<void> { | ||||||
|     logger.info(`Delete newer cpfp clusters from height ${height} from the database`); |     logger.info(`Delete newer cpfp clusters from height ${height} from the database`); | ||||||
|     try { |     try { | ||||||
| @ -122,6 +142,37 @@ class CpfpRepository { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   public async $deleteClustersAt(height: number): Promise<void> { | ||||||
|  |     logger.info(`Delete cpfp clusters at height ${height} from the database`); | ||||||
|  |     try { | ||||||
|  |       const [rows] = await DB.query( | ||||||
|  |         ` | ||||||
|  |           SELECT txs, height, root from compact_cpfp_clusters | ||||||
|  |           WHERE height = ? | ||||||
|  |         `,
 | ||||||
|  |         [height] | ||||||
|  |       ) as RowDataPacket[][]; | ||||||
|  |       if (rows?.length) { | ||||||
|  |         for (const clusterToDelete of rows) { | ||||||
|  |           const txs = this.unpack(clusterToDelete?.txs); | ||||||
|  |           for (const tx of txs) { | ||||||
|  |             await transactionRepository.$removeTransaction(tx.txid); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       await DB.query( | ||||||
|  |         ` | ||||||
|  |           DELETE from compact_cpfp_clusters | ||||||
|  |           WHERE height = ? | ||||||
|  |         `,
 | ||||||
|  |         [height] | ||||||
|  |       ); | ||||||
|  |     } catch (e: any) { | ||||||
|  |       logger.err(`Cannot delete cpfp clusters from db. Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|  |       throw e; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   // insert a dummy row to mark that we've indexed as far as this block
 |   // insert a dummy row to mark that we've indexed as far as this block
 | ||||||
|   public async $insertProgressMarker(height: number): Promise<void> { |   public async $insertProgressMarker(height: number): Promise<void> { | ||||||
|     try { |     try { | ||||||
| @ -190,6 +241,32 @@ class CpfpRepository { | |||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // returns `true` if two sets of CPFP clusters are deeply identical
 | ||||||
|  |   public compareClusters(clustersA: CpfpCluster[], clustersB: CpfpCluster[]): boolean { | ||||||
|  |     if (clustersA.length !== clustersB.length) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     clustersA = clustersA.sort((a,b) => a.root.localeCompare(b.root)); | ||||||
|  |     clustersB = clustersB.sort((a,b) => a.root.localeCompare(b.root)); | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < clustersA.length; i++) { | ||||||
|  |       if (clustersA[i].root !== clustersB[i].root) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if (clustersA[i].txs.length !== clustersB[i].txs.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       for (let j = 0; j < clustersA[i].txs.length; j++) { | ||||||
|  |         if (clustersA[i].txs[j].txid !== clustersB[i].txs[j].txid) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default new CpfpRepository(); | export default new CpfpRepository(); | ||||||
| @ -50,10 +50,10 @@ class PoolsUpdater { | |||||||
| 
 | 
 | ||||||
|       // See backend README for more details about the mining pools update process
 |       // See backend README for more details about the mining pools update process
 | ||||||
|       if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
 |       if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
 | ||||||
|         config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
 |         config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
 | ||||||
|         !process.env.npm_config_update_pools // We're not manually updating mining pool
 |         !process.env.npm_config_update_pools // We're not manually updating mining pool
 | ||||||
|       ) { |       ) { | ||||||
|         logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_BLOCK_REINDEXING is disabled`); |         logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`); | ||||||
|         logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); |         logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import config from '../../config'; | ||||||
| import { query } from '../../utils/axios-query'; | import { query } from '../../utils/axios-query'; | ||||||
| import { ConversionFeed, ConversionRates } from '../price-updater'; | import { ConversionFeed, ConversionRates } from '../price-updater'; | ||||||
| 
 | 
 | ||||||
| @ -37,15 +38,26 @@ const emptyRates = { | |||||||
|   ZAR: -1, |   ZAR: -1, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class FreeCurrencyApi implements ConversionFeed { | type PaidCurrencyData = { | ||||||
|   private API_KEY: string; |   [key: string]: { | ||||||
| 
 |       code: string; | ||||||
|   constructor(apiKey: string) { |       value: number; | ||||||
|     this.API_KEY = apiKey; |  | ||||||
|   } |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type FreeCurrencyData = { | ||||||
|  |   [key: string]: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | class FreeCurrencyApi implements ConversionFeed { | ||||||
|  |   private API_KEY = config.FIAT_PRICE.API_KEY; | ||||||
|  |   private PAID = config.FIAT_PRICE.PAID; | ||||||
|  |   private API_URL_PREFIX: string = this.PAID ? `https://api.currencyapi.com/v3/` : `https://api.freecurrencyapi.com/v1/`; | ||||||
|  | 
 | ||||||
|  |   constructor() { } | ||||||
| 
 | 
 | ||||||
|   public async $getQuota(): Promise<any> { |   public async $getQuota(): Promise<any> { | ||||||
|     const response = await query(`https://api.freecurrencyapi.com/v1/status?apikey=${this.API_KEY}`); |     const response = await query(`${this.API_URL_PREFIX}status?apikey=${this.API_KEY}`); | ||||||
|     if (response && response['quotas']) { |     if (response && response['quotas']) { | ||||||
|       return response['quotas']; |       return response['quotas']; | ||||||
|     } |     } | ||||||
| @ -53,21 +65,36 @@ class FreeCurrencyApi implements ConversionFeed { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $fetchLatestConversionRates(): Promise<ConversionRates> { |   public async $fetchLatestConversionRates(): Promise<ConversionRates> { | ||||||
|     const response = await query(`https://api.freecurrencyapi.com/v1/latest?apikey=${this.API_KEY}`); |     const response = await query(`${this.API_URL_PREFIX}latest?apikey=${this.API_KEY}`); | ||||||
|     if (response && response['data']) { |     if (response && response['data']) { | ||||||
|  |       if (this.PAID) { | ||||||
|  |         response['data'] = this.convertData(response['data']); | ||||||
|  |       } | ||||||
|       return response['data']; |       return response['data']; | ||||||
|     } |     } | ||||||
|     return emptyRates; |     return emptyRates; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async $fetchConversionRates(date: string): Promise<ConversionRates> { |   public async $fetchConversionRates(date: string): Promise<ConversionRates> { | ||||||
|     const response = await query(`https://api.freecurrencyapi.com/v1/historical?date=${date}&apikey=${this.API_KEY}`); |     const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true); | ||||||
|     if (response && response['data'] && response['data'][date]) { |     if (response && response['data'] && (response['data'][date] || this.PAID)) { | ||||||
|  |       if (this.PAID) { | ||||||
|  |         response['data'] = this.convertData(response['data']); | ||||||
|  |         response['data'][response['meta'].last_updated_at.substr(0, 10)] = response['data']; | ||||||
|  |       } | ||||||
|       return response['data'][date]; |       return response['data'][date]; | ||||||
|     } |     } | ||||||
|     return emptyRates; |     return emptyRates; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private convertData(data: PaidCurrencyData): FreeCurrencyData { | ||||||
|  |     const simplifiedData: FreeCurrencyData = {}; | ||||||
|  |     for (const key in data) { | ||||||
|  |       simplifiedData[key] = data[key].value; | ||||||
|  |     } | ||||||
|  |     return simplifiedData; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default FreeCurrencyApi; | export default FreeCurrencyApi; | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ class PriceUpdater { | |||||||
|   private currencyConversionFeed: ConversionFeed | undefined; |   private currencyConversionFeed: ConversionFeed | undefined; | ||||||
|   private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR']; |   private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR']; | ||||||
|   private lastTimeConversionsRatesFetched: number = 0; |   private lastTimeConversionsRatesFetched: number = 0; | ||||||
|   private latestConversionsRatesFromFeed: ConversionRates = {}; |   private latestConversionsRatesFromFeed: ConversionRates = { USD: -1 }; | ||||||
|   private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined; |   private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined; | ||||||
| 
 | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
| @ -71,7 +71,7 @@ class PriceUpdater { | |||||||
|     this.feeds.push(new BitfinexApi()); |     this.feeds.push(new BitfinexApi()); | ||||||
|     this.feeds.push(new GeminiApi()); |     this.feeds.push(new GeminiApi()); | ||||||
| 
 | 
 | ||||||
|     this.currencyConversionFeed = new FreeCurrencyApi(config.FIAT_PRICE.API_KEY); |     this.currencyConversionFeed = new FreeCurrencyApi(); | ||||||
|     this.setCyclePosition(); |     this.setCyclePosition(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -157,9 +157,9 @@ class PriceUpdater { | |||||||
|       try { |       try { | ||||||
|         this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates(); |         this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates(); | ||||||
|         this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000); |         this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000); | ||||||
|         logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`); |         logger.debug(`Fetched currencies conversion rates from conversions API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`); | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`); |         logger.err(`Cannot fetch conversion rates from conversions API. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -408,17 +408,17 @@ class PriceUpdater { | |||||||
|     try { |     try { | ||||||
|       const remainingQuota = await this.currencyConversionFeed?.$getQuota(); |       const remainingQuota = await this.currencyConversionFeed?.$getQuota(); | ||||||
|       if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
 |       if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
 | ||||||
|         logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining); |         logger.debug(`Not enough conversions API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining); | ||||||
|         this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
 |         this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
 | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`); |       logger.err(`Cannot fetch conversions API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.additionalCurrenciesHistoryRunning = true; |     this.additionalCurrenciesHistoryRunning = true; | ||||||
|     logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining); |     logger.debug(`Inserting missing historical conversion rates using conversions API to fill ${priceTimesToFill.length} rows`, logger.tags.mining); | ||||||
| 
 | 
 | ||||||
|     let conversionRates: { [timestamp: number]: ConversionRates } = {}; |     let conversionRates: { [timestamp: number]: ConversionRates } = {}; | ||||||
|     let totalInserted = 0; |     let totalInserted = 0; | ||||||
| @ -430,10 +430,23 @@ class PriceUpdater { | |||||||
|       const month = new Date(priceTime.time * 1000).getMonth(); |       const month = new Date(priceTime.time * 1000).getMonth(); | ||||||
|       const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000; |       const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000; | ||||||
|       if (conversionRates[yearMonthTimestamp] === undefined) { |       if (conversionRates[yearMonthTimestamp] === undefined) { | ||||||
|         conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 }; |         try { | ||||||
|         if (conversionRates[yearMonthTimestamp]['USD'] < 0) { |           if (year === new Date().getFullYear() && month === new Date().getMonth()) { // For rows in the current month, we use the latest conversion rates
 | ||||||
|           logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining); |             conversionRates[yearMonthTimestamp] = this.latestConversionsRatesFromFeed; | ||||||
|           this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000); |           } else { | ||||||
|  |             conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-15`) || { USD: -1 }; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (conversionRates[yearMonthTimestamp]['USD'] < 0) { | ||||||
|  |             throw new Error('Incorrect USD conversion rate'); | ||||||
|  |           } | ||||||
|  |         } catch (e) { | ||||||
|  |           if ((e instanceof Error ? e.message : '').includes('429')) { // Continue 60 seconds later if and only if error is 429
 | ||||||
|  |             this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000); | ||||||
|  |             logger.info(`Got a 429 error from conversions API. This is expected to happen a few times during the initial historical price insertion, process will resume in 60 seconds.`, logger.tags.mining); | ||||||
|  |           } else { | ||||||
|  |             logger.err(`Cannot fetch conversion rates from conversions API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01, trying again next day. Error: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); | ||||||
|  |           } | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ import config from '../config'; | |||||||
| import logger from '../logger'; | import logger from '../logger'; | ||||||
| import * as https from 'https'; | import * as https from 'https'; | ||||||
| 
 | 
 | ||||||
| export async function query(path): Promise<object | undefined> { | export async function query(path, throwOnFail: boolean = false): Promise<object | undefined> { | ||||||
|  type axiosOptions = { |  type axiosOptions = { | ||||||
|    headers: { |    headers: { | ||||||
|      'User-Agent': string |      'User-Agent': string | ||||||
| @ -21,6 +21,7 @@ export async function query(path): Promise<object | undefined> { | |||||||
|    timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000 |    timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000 | ||||||
|  }; |  }; | ||||||
|  let retry = 0; |  let retry = 0; | ||||||
|  |  let lastError: any = null; | ||||||
| 
 | 
 | ||||||
|  while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) { |  while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) { | ||||||
|    try { |    try { | ||||||
| @ -50,6 +51,7 @@ export async function query(path): Promise<object | undefined> { | |||||||
|      } |      } | ||||||
|      return data.data; |      return data.data; | ||||||
|    } catch (e) { |    } catch (e) { | ||||||
|  |      lastError = e; | ||||||
|      logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e)); |      logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e)); | ||||||
|      retry++; |      retry++; | ||||||
|    } |    } | ||||||
| @ -59,5 +61,10 @@ export async function query(path): Promise<object | undefined> { | |||||||
|  } |  } | ||||||
| 
 | 
 | ||||||
|  logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`); |  logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`); | ||||||
|  | 
 | ||||||
|  |  if (throwOnFail && lastError) { | ||||||
|  |     throw lastError; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  return undefined; |  return undefined; | ||||||
| } | } | ||||||
|  | |||||||
| @ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb | |||||||
|   if (!opN) { |   if (!opN) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (!opN.startsWith('OP_PUSHNUM_')) { |   if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); |   const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); | ||||||
| @ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb | |||||||
|   if (!opM) { |   if (!opM) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (!opM.startsWith('OP_PUSHNUM_')) { |   if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); |   const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								contributors/bitcoinmechanic.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/bitcoinmechanic.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022. | ||||||
|  | 
 | ||||||
|  | Signed: bitcoinmechanic | ||||||
							
								
								
									
										3
									
								
								contributors/daweilv.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/daweilv.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 7, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: daweilv | ||||||
							
								
								
									
										3
									
								
								contributors/hans-crypto.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/hans-crypto.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 21, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: hans-crypto | ||||||
							
								
								
									
										3
									
								
								contributors/henrialb.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/henrialb.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of April 12, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: henrialb | ||||||
							
								
								
									
										3
									
								
								contributors/jlopp.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/jlopp.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 12, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: jlopp | ||||||
							
								
								
									
										3
									
								
								contributors/mackalex.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/mackalex.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 18th, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: mackalex | ||||||
							
								
								
									
										3
									
								
								contributors/svrgnty.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/svrgnty.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 9, 2024. | ||||||
|  | 
 | ||||||
|  | Signed: svrgnty | ||||||
| @ -106,7 +106,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over | |||||||
|     "EXTERNAL_ASSETS": [], |     "EXTERNAL_ASSETS": [], | ||||||
|     "STDOUT_LOG_MIN_PRIORITY": "info", |     "STDOUT_LOG_MIN_PRIORITY": "info", | ||||||
|     "INDEXING_BLOCKS_AMOUNT": false, |     "INDEXING_BLOCKS_AMOUNT": false, | ||||||
|     "AUTOMATIC_BLOCK_REINDEXING": false, |     "AUTOMATIC_POOLS_UPDATE": false, | ||||||
|     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", |     "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", | ||||||
|     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", |     "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", | ||||||
|     "CPFP_INDEXING": false, |     "CPFP_INDEXING": false, | ||||||
| @ -137,7 +137,7 @@ Corresponding `docker-compose.yml` overrides: | |||||||
|       MEMPOOL_EXTERNAL_ASSETS: "" |       MEMPOOL_EXTERNAL_ASSETS: "" | ||||||
|       MEMPOOL_STDOUT_LOG_MIN_PRIORITY: "" |       MEMPOOL_STDOUT_LOG_MIN_PRIORITY: "" | ||||||
|       MEMPOOL_INDEXING_BLOCKS_AMOUNT: "" |       MEMPOOL_INDEXING_BLOCKS_AMOUNT: "" | ||||||
|       MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: "" |       MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" | ||||||
|       MEMPOOL_POOLS_JSON_URL: "" |       MEMPOOL_POOLS_JSON_URL: "" | ||||||
|       MEMPOOL_POOLS_JSON_TREE_URL: "" |       MEMPOOL_POOLS_JSON_TREE_URL: "" | ||||||
|       MEMPOOL_CPFP_INDEXING: "" |       MEMPOOL_CPFP_INDEXING: "" | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| FROM node:20.12.0-buster-slim AS builder | FROM node:20.15.0-buster-slim AS builder | ||||||
| 
 | 
 | ||||||
| ARG commitHash | ARG commitHash | ||||||
| ENV MEMPOOL_COMMIT_HASH=${commitHash} | ENV MEMPOOL_COMMIT_HASH=${commitHash} | ||||||
| @ -24,7 +24,7 @@ RUN npm install --omit=dev --omit=optional | |||||||
| WORKDIR /build | WORKDIR /build | ||||||
| RUN npm run package | RUN npm run package | ||||||
| 
 | 
 | ||||||
| FROM node:20.12.0-buster-slim | FROM node:20.15.0-buster-slim | ||||||
| 
 | 
 | ||||||
| WORKDIR /backend | WORKDIR /backend | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ | |||||||
|     "OFFICIAL": __MEMPOOL_OFFICIAL__, |     "OFFICIAL": __MEMPOOL_OFFICIAL__, | ||||||
|     "HTTP_PORT": __MEMPOOL_HTTP_PORT__, |     "HTTP_PORT": __MEMPOOL_HTTP_PORT__, | ||||||
|     "SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__, |     "SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__, | ||||||
|  |     "UNIX_SOCKET_PATH": "__MEMPOOL_UNIX_SOCKET_PATH__", | ||||||
|     "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__", |     "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__", | ||||||
|     "POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__, |     "POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__, | ||||||
|     "CACHE_DIR": "__MEMPOOL_CACHE_DIR__", |     "CACHE_DIR": "__MEMPOOL_CACHE_DIR__", | ||||||
| @ -24,7 +25,7 @@ | |||||||
|     "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, |     "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, | ||||||
|     "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__, |     "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__, | ||||||
|     "GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__, |     "GOGGLES_INDEXING": __MEMPOOL_GOGGLES_INDEXING__, | ||||||
|     "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__, |     "AUTOMATIC_POOLS_UPDATE": __MEMPOOL_AUTOMATIC_POOLS_UPDATE__, | ||||||
|     "AUDIT": __MEMPOOL_AUDIT__, |     "AUDIT": __MEMPOOL_AUDIT__, | ||||||
|     "RUST_GBT": __MEMPOOL_RUST_GBT__, |     "RUST_GBT": __MEMPOOL_RUST_GBT__, | ||||||
|     "LIMIT_GBT": __MEMPOOL_LIMIT_GBT__, |     "LIMIT_GBT": __MEMPOOL_LIMIT_GBT__, | ||||||
| @ -59,7 +60,8 @@ | |||||||
|     "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, |     "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, | ||||||
|     "REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__, |     "REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__, | ||||||
|     "FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__, |     "FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__, | ||||||
|     "FALLBACK": __ESPLORA_FALLBACK__ |     "FALLBACK": __ESPLORA_FALLBACK__, | ||||||
|  |     "MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__ | ||||||
|   }, |   }, | ||||||
|   "SECOND_CORE_RPC": { |   "SECOND_CORE_RPC": { | ||||||
|     "HOST": "__SECOND_CORE_RPC_HOST__", |     "HOST": "__SECOND_CORE_RPC_HOST__", | ||||||
| @ -136,6 +138,8 @@ | |||||||
|     "ENABLED": __REPLICATION_ENABLED__, |     "ENABLED": __REPLICATION_ENABLED__, | ||||||
|     "AUDIT": __REPLICATION_AUDIT__, |     "AUDIT": __REPLICATION_AUDIT__, | ||||||
|     "AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__, |     "AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__, | ||||||
|  |     "STATISTICS": __REPLICATION_STATISTICS__, | ||||||
|  |     "STATISTICS_START_TIME": __REPLICATION_STATISTICS_START_TIME__, | ||||||
|     "SERVERS": __REPLICATION_SERVERS__ |     "SERVERS": __REPLICATION_SERVERS__ | ||||||
|   }, |   }, | ||||||
|   "MEMPOOL_SERVICES": { |   "MEMPOOL_SERVICES": { | ||||||
| @ -149,6 +153,7 @@ | |||||||
|   }, |   }, | ||||||
|   "FIAT_PRICE": { |   "FIAT_PRICE": { | ||||||
|     "ENABLED": __FIAT_PRICE_ENABLED__, |     "ENABLED": __FIAT_PRICE_ENABLED__, | ||||||
|  |     "PAID": __FIAT_PRICE_PAID__, | ||||||
|     "API_KEY": "__FIAT_PRICE_API_KEY__" |     "API_KEY": "__FIAT_PRICE_API_KEY__" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ __MEMPOOL_ENABLED__=${MEMPOOL_ENABLED:=true} | |||||||
| __MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false} | __MEMPOOL_OFFICIAL__=${MEMPOOL_OFFICIAL:=false} | ||||||
| __MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999} | __MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999} | ||||||
| __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0} | __MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0} | ||||||
|  | __MEMPOOL_UNIX_SOCKET_PATH__=${MEMPOOL_UNIX_SOCKET_PATH:=""} | ||||||
| __MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/} | __MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/} | ||||||
| __MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000} | __MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000} | ||||||
| __MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache} | __MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache} | ||||||
| @ -25,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1} | |||||||
| __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} | __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} | ||||||
| __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} | __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} | ||||||
| __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} | __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} | ||||||
| __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} | __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} | ||||||
| __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} | __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} | ||||||
| __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} | __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} | ||||||
| __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} | __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} | ||||||
| @ -61,6 +62,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} | |||||||
| __ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000} | __ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000} | ||||||
| __ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000} | __ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000} | ||||||
| __ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]} | __ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]} | ||||||
|  | __ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2} | ||||||
| 
 | 
 | ||||||
| # SECOND_CORE_RPC | # SECOND_CORE_RPC | ||||||
| __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1} | __SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1} | ||||||
| @ -137,19 +139,22 @@ __MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""} | |||||||
| __REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false} | __REPLICATION_ENABLED__=${REPLICATION_ENABLED:=false} | ||||||
| __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false} | __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=false} | ||||||
| __REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000} | __REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000} | ||||||
|  | __REPLICATION_STATISTICS__=${REPLICATION_STATISTICS:=false} | ||||||
|  | __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=1481932800} | ||||||
| __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} | __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} | ||||||
| 
 | 
 | ||||||
| # MEMPOOL_SERVICES | # MEMPOOL_SERVICES | ||||||
| __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""} | __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} | ||||||
| __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} | __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} | ||||||
| 
 | 
 | ||||||
| # REDIS | # REDIS | ||||||
| __REDIS_ENABLED__=${REDIS_ENABLED:=false} | __REDIS_ENABLED__=${REDIS_ENABLED:=false} | ||||||
| __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} | __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} | ||||||
| __REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000} | __REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000} | ||||||
| 
 | 
 | ||||||
| # FIAT_PRICE | # FIAT_PRICE | ||||||
| __FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true} | __FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true} | ||||||
|  | __FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false} | ||||||
| __FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""} | __FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""} | ||||||
| 
 | 
 | ||||||
| mkdir -p "${__MEMPOOL_CACHE_DIR__}" | mkdir -p "${__MEMPOOL_CACHE_DIR__}" | ||||||
| @ -160,6 +165,7 @@ sed -i "s!__MEMPOOL_ENABLED__!${__MEMPOOL_ENABLED__}!g" mempool-config.json | |||||||
| sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json | sed -i "s!__MEMPOOL_OFFICIAL__!${__MEMPOOL_OFFICIAL__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json | sed -i "s!__MEMPOOL_HTTP_PORT__!${__MEMPOOL_HTTP_PORT__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json | sed -i "s!__MEMPOOL_SPAWN_CLUSTER_PROCS__!${__MEMPOOL_SPAWN_CLUSTER_PROCS__}!g" mempool-config.json | ||||||
|  | sed -i "s!__MEMPOOL_UNIX_SOCKET_PATH__!${__MEMPOOL_UNIX_SOCKET_PATH__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json | sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json | sed -i "s!__MEMPOOL_POLL_RATE_MS__!${__MEMPOOL_POLL_RATE_MS__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json | sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json | ||||||
| @ -178,7 +184,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me | |||||||
| sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json | sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json | sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json | sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__!${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}!g" mempool-config.json | sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json | sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json | sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json | ||||||
| sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json | sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json | ||||||
| @ -211,6 +217,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE | |||||||
| sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json | sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json | ||||||
| sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json | sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json | ||||||
| sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json | sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json | ||||||
|  | sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!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_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json | ||||||
| sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json | sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json | ||||||
| @ -281,6 +288,8 @@ sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.jso | |||||||
| sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json | sed -i "s!__REPLICATION_ENABLED__!${__REPLICATION_ENABLED__}!g" mempool-config.json | ||||||
| sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json | sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json | ||||||
| sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json | sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json | ||||||
|  | sed -i "s!__REPLICATION_STATISTICS__!${__REPLICATION_STATISTICS__}!g" mempool-config.json | ||||||
|  | sed -i "s!__REPLICATION_STATISTICS_START_TIME__!${__REPLICATION_STATISTICS_START_TIME__}!g" mempool-config.json | ||||||
| sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json | sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json | ||||||
| 
 | 
 | ||||||
| # MEMPOOL_SERVICES | # MEMPOOL_SERVICES | ||||||
| @ -294,6 +303,7 @@ sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" | |||||||
| 
 | 
 | ||||||
| # FIAT_PRICE | # FIAT_PRICE | ||||||
| sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json | sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.json | ||||||
|  | sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json | ||||||
| sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json | sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json | ||||||
| 
 | 
 | ||||||
| node /backend/package/index.js | node /backend/package/index.js | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| FROM node:20.12.0-buster-slim AS builder | FROM node:20.15.0-buster-slim AS builder | ||||||
| 
 | 
 | ||||||
| ARG commitHash | ARG commitHash | ||||||
| ENV DOCKER_COMMIT_HASH=${commitHash} | ENV DOCKER_COMMIT_HASH=${commitHash} | ||||||
| @ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional | |||||||
| 
 | 
 | ||||||
| RUN npm run build | RUN npm run build | ||||||
| 
 | 
 | ||||||
| FROM nginx:1.25.4-alpine | FROM nginx:1.27.0-alpine | ||||||
| 
 | 
 | ||||||
| WORKDIR /patch | WORKDIR /patch | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,7 +16,9 @@ fi | |||||||
| 
 | 
 | ||||||
| # Runtime overrides - read env vars defined in docker compose | # Runtime overrides - read env vars defined in docker compose | ||||||
| 
 | 
 | ||||||
|  | __MAINNET_ENABLED__=${MAINNET_ENABLED:=true} | ||||||
| __TESTNET_ENABLED__=${TESTNET_ENABLED:=false} | __TESTNET_ENABLED__=${TESTNET_ENABLED:=false} | ||||||
|  | __TESTNET4_ENABLED__=${TESTNET_ENABLED:=false} | ||||||
| __SIGNET_ENABLED__=${SIGNET_ENABLED:=false} | __SIGNET_ENABLED__=${SIGNET_ENABLED:=false} | ||||||
| __LIQUID_ENABLED__=${LIQUID_ENABLED:=false} | __LIQUID_ENABLED__=${LIQUID_ENABLED:=false} | ||||||
| __LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false} | __LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false} | ||||||
| @ -28,6 +30,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999} | |||||||
| __BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000} | __BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000} | ||||||
| __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8} | __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8} | ||||||
| __BASE_MODULE__=${BASE_MODULE:=mempool} | __BASE_MODULE__=${BASE_MODULE:=mempool} | ||||||
|  | __ROOT_NETWORK__=${ROOT_NETWORK:=} | ||||||
| __MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space} | __MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space} | ||||||
| __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} | __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} | ||||||
| __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} | __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} | ||||||
| @ -37,12 +40,16 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} | |||||||
| __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} | __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0} | ||||||
| __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} | __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} | ||||||
| __ACCELERATOR__=${ACCELERATOR:=false} | __ACCELERATOR__=${ACCELERATOR:=false} | ||||||
|  | __ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true} | ||||||
|  | __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services} | ||||||
| __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} | __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} | ||||||
| __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} | __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} | ||||||
| __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} | __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} | ||||||
| 
 | 
 | ||||||
| # Export as environment variables to be used by envsubst | # Export as environment variables to be used by envsubst | ||||||
|  | export __MAINNET_ENABLED__ | ||||||
| export __TESTNET_ENABLED__ | export __TESTNET_ENABLED__ | ||||||
|  | export __TESTNET4_ENABLED__ | ||||||
| export __SIGNET_ENABLED__ | export __SIGNET_ENABLED__ | ||||||
| export __LIQUID_ENABLED__ | export __LIQUID_ENABLED__ | ||||||
| export __LIQUID_TESTNET_ENABLED__ | export __LIQUID_TESTNET_ENABLED__ | ||||||
| @ -54,6 +61,7 @@ export __NGINX_PORT__ | |||||||
| export __BLOCK_WEIGHT_UNITS__ | export __BLOCK_WEIGHT_UNITS__ | ||||||
| export __MEMPOOL_BLOCKS_AMOUNT__ | export __MEMPOOL_BLOCKS_AMOUNT__ | ||||||
| export __BASE_MODULE__ | export __BASE_MODULE__ | ||||||
|  | export __ROOT_NETWORK__ | ||||||
| export __MEMPOOL_WEBSITE_URL__ | export __MEMPOOL_WEBSITE_URL__ | ||||||
| export __LIQUID_WEBSITE_URL__ | export __LIQUID_WEBSITE_URL__ | ||||||
| export __MINING_DASHBOARD__ | export __MINING_DASHBOARD__ | ||||||
| @ -63,6 +71,8 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ | |||||||
| export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ | export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ | ||||||
| export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ | export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ | ||||||
| export __ACCELERATOR__ | export __ACCELERATOR__ | ||||||
|  | export __ACCELERATOR_BUTTON__ | ||||||
|  | export __SERVICES_API__ | ||||||
| export __PUBLIC_ACCELERATIONS__ | export __PUBLIC_ACCELERATIONS__ | ||||||
| export __HISTORICAL_PRICE__ | export __HISTORICAL_PRICE__ | ||||||
| export __ADDITIONAL_CURRENCIES__ | export __ADDITIONAL_CURRENCIES__ | ||||||
|  | |||||||
| @ -34,6 +34,8 @@ | |||||||
|     "prefer-rest-params": 1, |     "prefer-rest-params": 1, | ||||||
|     "quotes": [1, "single", { "allowTemplateLiterals": true }], |     "quotes": [1, "single", { "allowTemplateLiterals": true }], | ||||||
|     "semi": 1, |     "semi": 1, | ||||||
|     "eqeqeq": 1 |     "curly": [1, "all"], | ||||||
|  |     "eqeqeq": 1, | ||||||
|  |     "no-trailing-spaces": 1 | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -63,6 +63,7 @@ src/resources/pools.json | |||||||
| src/resources/mining-pools/* | src/resources/mining-pools/* | ||||||
| src/resources/**/*.mp4 | src/resources/**/*.mp4 | ||||||
| src/resources/**/*.vtt | src/resources/**/*.vtt | ||||||
|  | src/resources/customize.js | ||||||
| 
 | 
 | ||||||
| # environment config | # environment config | ||||||
| mempool-frontend-config.json | mempool-frontend-config.json | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ $ npm run config:defaults:liquid | |||||||
| 
 | 
 | ||||||
| ### 3. Run the Frontend | ### 3. Run the Frontend | ||||||
| 
 | 
 | ||||||
| _Make sure to use Node.js 16.10 and npm 7._ | _Make sure to use Node.js 20.x and npm 9.x or newer._ | ||||||
| 
 | 
 | ||||||
| Install project dependencies and run the frontend server: | Install project dependencies and run the frontend server: | ||||||
| 
 | 
 | ||||||
| @ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already. | |||||||
| 
 | 
 | ||||||
| ### 1. Build the Frontend | ### 1. Build the Frontend | ||||||
| 
 | 
 | ||||||
| _Make sure to use Node.js 16.10 and npm 7._ | _Make sure to use Node.js 20.x and npm 9.x or newer._ | ||||||
| 
 | 
 | ||||||
| Build the frontend: | Build the frontend: | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -54,6 +54,10 @@ | |||||||
|             "translation": "src/locale/messages.fr.xlf", |             "translation": "src/locale/messages.fr.xlf", | ||||||
|             "baseHref": "/fr/" |             "baseHref": "/fr/" | ||||||
|           }, |           }, | ||||||
|  |           "hr": { | ||||||
|  |             "translation": "src/locale/messages.hr.xlf", | ||||||
|  |             "baseHref": "/hr/" | ||||||
|  |           }, | ||||||
|           "ja": { |           "ja": { | ||||||
|             "translation": "src/locale/messages.ja.xlf", |             "translation": "src/locale/messages.ja.xlf", | ||||||
|             "baseHref": "/ja/" |             "baseHref": "/ja/" | ||||||
| @ -166,10 +170,26 @@ | |||||||
|               "src/resources", |               "src/resources", | ||||||
|               "src/robots.txt", |               "src/robots.txt", | ||||||
|               "src/config.js", |               "src/config.js", | ||||||
|  |               "src/customize.js", | ||||||
|               "src/config.template.js" |               "src/config.template.js" | ||||||
|             ], |             ], | ||||||
|             "styles": [ |             "styles": [ | ||||||
|               "src/styles.scss", |               "src/styles.scss", | ||||||
|  |               { | ||||||
|  |                 "input": "src/theme-contrast.scss", | ||||||
|  |                 "bundleName": "contrast", | ||||||
|  |                 "inject": false | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 "input": "src/theme-wiz.scss", | ||||||
|  |                 "bundleName": "wiz", | ||||||
|  |                 "inject": false | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 "input": "src/theme-bukele.scss", | ||||||
|  |                 "bundleName": "bukele", | ||||||
|  |                 "inject": false | ||||||
|  |               }, | ||||||
|               "node_modules/@fortawesome/fontawesome-svg-core/styles.css" |               "node_modules/@fortawesome/fontawesome-svg-core/styles.css" | ||||||
|             ], |             ], | ||||||
|             "vendorChunk": true, |             "vendorChunk": true, | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								frontend/custom-sv-config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								frontend/custom-sv-config.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | { | ||||||
|  |   "theme": "bukele", | ||||||
|  |   "enterprise": "onbtc", | ||||||
|  |   "branding": { | ||||||
|  |     "name": "onbtc", | ||||||
|  |     "title": "Bitcoin Office", | ||||||
|  |     "site_id": 19, | ||||||
|  |     "header_img": "/resources/onbtclogo.svg", | ||||||
|  |     "footer_img": "/resources/onbtclogo.svg", | ||||||
|  |     "rounded_corner": true | ||||||
|  |   }, | ||||||
|  |   "dashboard": { | ||||||
|  |     "widgets": [ | ||||||
|  |       { | ||||||
|  |         "component": "fees", | ||||||
|  |         "mobileOrder": 4 | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "component": "balance", | ||||||
|  |         "mobileOrder": 1, | ||||||
|  |         "props": { | ||||||
|  |           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "component": "twitter", | ||||||
|  |         "mobileOrder": 5, | ||||||
|  |         "props": { | ||||||
|  |           "handle": "nayibbukele" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "component": "address", | ||||||
|  |         "mobileOrder": 2, | ||||||
|  |         "props": { | ||||||
|  |           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", | ||||||
|  |           "period": "1m" | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "component": "blocks" | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         "component": "addressTransactions", | ||||||
|  |         "mobileOrder": 3, | ||||||
|  |         "props": { | ||||||
|  |           "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -45,6 +45,7 @@ describe('Liquid', () => { | |||||||
| 
 | 
 | ||||||
|     it('loads a specific block page', () => { |     it('loads a specific block page', () => { | ||||||
|       cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`); |       cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`); | ||||||
|  |       cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -71,20 +72,6 @@ describe('Liquid', () => { | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('renders unconfidential addresses correctly on mobile', () => { |  | ||||||
|       cy.viewport('iphone-6'); |  | ||||||
|       cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`); |  | ||||||
|       cy.waitForSkeletonGone(); |  | ||||||
|       //TODO: Add proper IDs for these selectors
 |  | ||||||
|       const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody'; |  | ||||||
|       const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)'; |  | ||||||
|       cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => { |  | ||||||
|         cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => { |  | ||||||
|           expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth)); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     describe('peg in/peg out', () => { |     describe('peg in/peg out', () => { | ||||||
|       it('loads peg in addresses', () => { |       it('loads peg in addresses', () => { | ||||||
|         cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`); |         cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`); | ||||||
|  | |||||||
| @ -46,7 +46,8 @@ describe('Liquid Testnet', () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('loads a specific block page', () => { |     it('loads a specific block page', () => { | ||||||
|       cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`); |       cy.visit(`${basePath}/block/fb4cbcbff3993ca4bf8caf657d55a23db5ed4ab1cfa33c489303c2e04e1c38e0`); | ||||||
|  |       cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -103,6 +103,7 @@ describe('Mainnet', () => { | |||||||
| 
 | 
 | ||||||
|     it('check op_return tx tooltip', () => { |     it('check op_return tx tooltip', () => { | ||||||
|       cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); |       cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); | ||||||
|  |       cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover'); |       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover'); | ||||||
|       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter'); |       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter'); | ||||||
| @ -111,9 +112,10 @@ describe('Mainnet', () => { | |||||||
| 
 | 
 | ||||||
|     it('check op_return coinbase tooltip', () => { |     it('check op_return coinbase tooltip', () => { | ||||||
|       cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); |       cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2'); | ||||||
|  |       cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|       cy.get('div > a > .badge').first().trigger('onmouseover'); |       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover'); | ||||||
|       cy.get('div > a > .badge').first().trigger('mouseenter'); |       cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter'); | ||||||
|       cy.get('.tooltip-inner').should('be.visible'); |       cy.get('.tooltip-inner').should('be.visible'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -142,13 +144,13 @@ describe('Mainnet', () => { | |||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       ['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => { |       ['BC1PQYQS', 'bc1PqYqS'].forEach((searchTerm) => { | ||||||
|         it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => { |         it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => { | ||||||
|           cy.visit('/'); |           cy.visit('/'); | ||||||
|           cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { |           cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { | ||||||
|             cy.get('app-search-results button.dropdown-item').should('have.length', 1); |             cy.get('app-search-results button.dropdown-item').should('have.length', 10); | ||||||
|             cy.get('app-search-results button.dropdown-item.active').click().then(() => { |             cy.get('app-search-results button.dropdown-item.active').click().then(() => { | ||||||
|               cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e'); |               cy.url().should('include', '/address/bc1pqyqs26fs4gnyw4aqttyjqa5ta7075zzfjftyz98qa8vdr49dh7fqm2zkv3'); | ||||||
|               cy.waitForSkeletonGone(); |               cy.waitForSkeletonGone(); | ||||||
|               cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); |               cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); | ||||||
|             }); |             }); | ||||||
| @ -156,13 +158,13 @@ describe('Mainnet', () => { | |||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       ['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => { |       ['BC1Q0003', 'bC1q0003'].forEach((searchTerm) => { | ||||||
|         it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => { |         it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => { | ||||||
|           cy.visit('/'); |           cy.visit('/'); | ||||||
|           cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { |           cy.get('.search-box-container > .form-control').type(searchTerm).then(() => { | ||||||
|             cy.get('app-search-results button.dropdown-item').should('have.length', 1); |             cy.get('app-search-results button.dropdown-item').should('have.length', 10); | ||||||
|             cy.get('app-search-results button.dropdown-item.active').click().then(() => { |             cy.get('app-search-results button.dropdown-item.active').click().then(() => { | ||||||
|               cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy'); |               cy.url().should('include', '/address/bc1q000303cgr9zazthut63kdktwtatfe206um8nyh'); | ||||||
|               cy.waitForSkeletonGone(); |               cy.waitForSkeletonGone(); | ||||||
|               cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); |               cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address'); | ||||||
|             }); |             }); | ||||||
| @ -283,6 +285,7 @@ describe('Mainnet', () => { | |||||||
|         it('loads genesis block and keypress arrow right', () => { |         it('loads genesis block and keypress arrow right', () => { | ||||||
|           cy.viewport('macbook-16'); |           cy.viewport('macbook-16'); | ||||||
|           cy.visit('/block/0'); |           cy.visit('/block/0'); | ||||||
|  |           cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.waitForPageIdle(); |           cy.waitForPageIdle(); | ||||||
| 
 | 
 | ||||||
| @ -295,6 +298,7 @@ describe('Mainnet', () => { | |||||||
|         it('loads genesis block and keypress arrow left', () => { |         it('loads genesis block and keypress arrow left', () => { | ||||||
|           cy.viewport('macbook-16'); |           cy.viewport('macbook-16'); | ||||||
|           cy.visit('/block/0'); |           cy.visit('/block/0'); | ||||||
|  |           cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.waitForPageIdle(); |           cy.waitForPageIdle(); | ||||||
| 
 | 
 | ||||||
| @ -323,6 +327,7 @@ describe('Mainnet', () => { | |||||||
|         it('loads genesis block and click on the arrow left', () => { |         it('loads genesis block and click on the arrow left', () => { | ||||||
|           cy.viewport('macbook-16'); |           cy.viewport('macbook-16'); | ||||||
|           cy.visit('/block/0'); |           cy.visit('/block/0'); | ||||||
|  |           cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.waitForPageIdle(); |           cy.waitForPageIdle(); | ||||||
|           cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); |           cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); | ||||||
| @ -339,7 +344,7 @@ describe('Mainnet', () => { | |||||||
|       cy.visit('/'); |       cy.visit('/'); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
| 
 | 
 | ||||||
|       cy.changeNetwork('testnet'); |       cy.changeNetwork('testnet4'); | ||||||
|       cy.changeNetwork('signet'); |       cy.changeNetwork('signet'); | ||||||
|       cy.changeNetwork('mainnet'); |       cy.changeNetwork('mainnet'); | ||||||
|     }); |     }); | ||||||
| @ -439,6 +444,7 @@ describe('Mainnet', () => { | |||||||
|     describe('blocks', () => { |     describe('blocks', () => { | ||||||
|       it('shows empty blocks properly', () => { |       it('shows empty blocks properly', () => { | ||||||
|         cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775'); |         cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.waitForPageIdle(); |         cy.waitForPageIdle(); | ||||||
|         cy.get('h2').invoke('text').should('equal', '1 transaction'); |         cy.get('h2').invoke('text').should('equal', '1 transaction'); | ||||||
| @ -446,6 +452,7 @@ describe('Mainnet', () => { | |||||||
| 
 | 
 | ||||||
|       it('expands and collapses the block details', () => { |       it('expands and collapses the block details', () => { | ||||||
|         cy.visit('/block/0'); |         cy.visit('/block/0'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.waitForPageIdle(); |         cy.waitForPageIdle(); | ||||||
|         cy.get('.btn.btn-outline-info').click().then(() => { |         cy.get('.btn.btn-outline-info').click().then(() => { | ||||||
| @ -458,6 +465,7 @@ describe('Mainnet', () => { | |||||||
|       }); |       }); | ||||||
|       it('shows blocks with no pagination', () => { |       it('shows blocks with no pagination', () => { | ||||||
|         cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4'); |         cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.waitForPageIdle(); |         cy.waitForPageIdle(); | ||||||
|         cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions'); |         cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions'); | ||||||
| @ -467,6 +475,7 @@ describe('Mainnet', () => { | |||||||
|       it('supports pagination on the block screen', () => { |       it('supports pagination on the block screen', () => { | ||||||
|         // 41 txs
 |         // 41 txs
 | ||||||
|         cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8'); |         cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.pagination-container a').invoke('text').then((text1) => { |         cy.get('.pagination-container a').invoke('text').then((text1) => { | ||||||
|           cy.get('.active + li').first().click().then(() => { |           cy.get('.active + li').first().click().then(() => { | ||||||
| @ -482,6 +491,7 @@ describe('Mainnet', () => { | |||||||
|       it('shows blocks pagination with 5 pages (desktop)', () => { |       it('shows blocks pagination with 5 pages (desktop)', () => { | ||||||
|         cy.viewport(760, 800); |         cy.viewport(760, 800); | ||||||
|         cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { |         cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { | ||||||
|  |           cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.waitForPageIdle(); |           cy.waitForPageIdle(); | ||||||
|         }); |         }); | ||||||
| @ -493,6 +503,7 @@ describe('Mainnet', () => { | |||||||
|       it('shows blocks pagination with 3 pages (mobile)', () => { |       it('shows blocks pagination with 3 pages (mobile)', () => { | ||||||
|         cy.viewport(669, 800); |         cy.viewport(669, 800); | ||||||
|         cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { |         cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => { | ||||||
|  |           cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|           cy.waitForSkeletonGone(); |           cy.waitForSkeletonGone(); | ||||||
|           cy.waitForPageIdle(); |           cy.waitForPageIdle(); | ||||||
|         }); |         }); | ||||||
| @ -532,16 +543,7 @@ describe('Mainnet', () => { | |||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         cy.get('.alert').should('be.visible'); |         cy.get('.alert-replaced').should('be.visible'); | ||||||
|         cy.get('.alert').invoke('css', 'width').then((alertWidth) => { |  | ||||||
|           cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         cy.get('.btn-warning').then(getRectangle).then((rectA) => { |  | ||||||
|           cy.get('.alert').then(getRectangle).then((rectB) => { |  | ||||||
|             expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false; |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('shows RBF transactions properly (desktop)', () => { |       it('shows RBF transactions properly (desktop)', () => { | ||||||
|  | |||||||
| @ -95,12 +95,14 @@ describe('Signet', () => { | |||||||
|     describe('blocks', () => { |     describe('blocks', () => { | ||||||
|       it('shows empty blocks properly', () => { |       it('shows empty blocks properly', () => { | ||||||
|         cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42'); |         cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('h2').invoke('text').should('equal', '1 transaction'); |         cy.get('h2').invoke('text').should('equal', '1 transaction'); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('expands and collapses the block details', () => { |       it('expands and collapses the block details', () => { | ||||||
|         cy.visit('/signet/block/0'); |         cy.visit('/signet/block/0'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.btn.btn-outline-info').click().then(() => { |         cy.get('.btn.btn-outline-info').click().then(() => { | ||||||
|           cy.get('#details').should('be.visible'); |           cy.get('#details').should('be.visible'); | ||||||
| @ -113,6 +115,7 @@ describe('Signet', () => { | |||||||
| 
 | 
 | ||||||
|       it('shows blocks with no pagination', () => { |       it('shows blocks with no pagination', () => { | ||||||
|         cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc'); |         cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('h2').invoke('text').should('equal', '13 transactions'); |         cy.get('h2').invoke('text').should('equal', '13 transactions'); | ||||||
|         cy.get('ul.pagination').first().children().should('have.length', 5); |         cy.get('ul.pagination').first().children().should('have.length', 5); | ||||||
| @ -121,6 +124,7 @@ describe('Signet', () => { | |||||||
|       it('supports pagination on the block screen', () => { |       it('supports pagination on the block screen', () => { | ||||||
|         // 43 txs
 |         // 43 txs
 | ||||||
|         cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6'); |         cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.header-bg.box > a').invoke('text').then((text1) => { |         cy.get('.header-bg.box > a').invoke('text').then((text1) => { | ||||||
|           cy.get('.active + li').first().click().then(() => { |           cy.get('.active + li').first().click().then(() => { | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { emitMempoolInfo } from '../../support/websocket'; | |||||||
| 
 | 
 | ||||||
| const baseModule = Cypress.env('BASE_MODULE'); | const baseModule = Cypress.env('BASE_MODULE'); | ||||||
| 
 | 
 | ||||||
| describe('Testnet', () => { | describe('Testnet4', () => { | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     cy.intercept('/api/block-height/*').as('block-height'); |     cy.intercept('/api/block-height/*').as('block-height'); | ||||||
|     cy.intercept('/api/block/*').as('block'); |     cy.intercept('/api/block/*').as('block'); | ||||||
| @ -13,7 +13,7 @@ describe('Testnet', () => { | |||||||
|   if (baseModule === 'mempool') { |   if (baseModule === 'mempool') { | ||||||
| 
 | 
 | ||||||
|     it('loads the dashboard', () => { |     it('loads the dashboard', () => { | ||||||
|       cy.visit('/testnet'); |       cy.visit('/testnet4'); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -25,7 +25,7 @@ describe('Testnet', () => { | |||||||
| 
 | 
 | ||||||
|     it.skip('loads the dashboard with the skeleton blocks', () => { |     it.skip('loads the dashboard with the skeleton blocks', () => { | ||||||
|       cy.mockMempoolSocket(); |       cy.mockMempoolSocket(); | ||||||
|       cy.visit('/testnet'); |       cy.visit('/testnet4'); | ||||||
|       cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); |       cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); | ||||||
|       cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); |       cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); | ||||||
|       cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); |       cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); | ||||||
| @ -45,7 +45,7 @@ describe('Testnet', () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('loads the pools screen', () => { |     it('loads the pools screen', () => { | ||||||
|       cy.visit('/testnet'); |       cy.visit('/testnet4'); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|       cy.get('#btn-pools').click().then(() => { |       cy.get('#btn-pools').click().then(() => { | ||||||
|         cy.wait(1000); |         cy.wait(1000); | ||||||
| @ -53,7 +53,7 @@ describe('Testnet', () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('loads the graphs screen', () => { |     it('loads the graphs screen', () => { | ||||||
|       cy.visit('/testnet'); |       cy.visit('/testnet4'); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|       cy.get('#btn-graphs').click().then(() => { |       cy.get('#btn-graphs').click().then(() => { | ||||||
|         cy.wait(1000); |         cy.wait(1000); | ||||||
| @ -63,7 +63,7 @@ describe('Testnet', () => { | |||||||
|     describe('tv mode', () => { |     describe('tv mode', () => { | ||||||
|       it('loads the tv screen - desktop', () => { |       it('loads the tv screen - desktop', () => { | ||||||
|         cy.viewport('macbook-16'); |         cy.viewport('macbook-16'); | ||||||
|         cy.visit('/testnet/graphs'); |         cy.visit('/testnet4/graphs'); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('#btn-tv').click().then(() => { |         cy.get('#btn-tv').click().then(() => { | ||||||
|           cy.wait(1000); |           cy.wait(1000); | ||||||
| @ -73,7 +73,7 @@ describe('Testnet', () => { | |||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('loads the tv screen - mobile', () => { |       it('loads the tv screen - mobile', () => { | ||||||
|         cy.visit('/testnet/graphs'); |         cy.visit('/testnet4/graphs'); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('#btn-tv').click().then(() => { |         cy.get('#btn-tv').click().then(() => { | ||||||
|           cy.viewport('iphone-6'); |           cy.viewport('iphone-6'); | ||||||
| @ -85,7 +85,7 @@ describe('Testnet', () => { | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     it('loads the api screen', () => { |     it('loads the api screen', () => { | ||||||
|       cy.visit('/testnet'); |       cy.visit('/testnet4'); | ||||||
|       cy.waitForSkeletonGone(); |       cy.waitForSkeletonGone(); | ||||||
|       cy.get('#btn-docs').click().then(() => { |       cy.get('#btn-docs').click().then(() => { | ||||||
|         cy.wait(1000); |         cy.wait(1000); | ||||||
| @ -94,13 +94,15 @@ describe('Testnet', () => { | |||||||
| 
 | 
 | ||||||
|     describe('blocks', () => { |     describe('blocks', () => { | ||||||
|       it('shows empty blocks properly', () => { |       it('shows empty blocks properly', () => { | ||||||
|         cy.visit('/testnet/block/0'); |         cy.visit('/testnet4/block/0'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('h2').invoke('text').should('equal', '1 transaction'); |         cy.get('h2').invoke('text').should('equal', '1 transaction'); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('expands and collapses the block details', () => { |       it('expands and collapses the block details', () => { | ||||||
|         cy.visit('/testnet/block/0'); |         cy.visit('/testnet4/block/0'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.btn.btn-outline-info').click().then(() => { |         cy.get('.btn.btn-outline-info').click().then(() => { | ||||||
|           cy.get('#details').should('be.visible'); |           cy.get('#details').should('be.visible'); | ||||||
| @ -112,15 +114,17 @@ describe('Testnet', () => { | |||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('shows blocks with no pagination', () => { |       it('shows blocks with no pagination', () => { | ||||||
|         cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea'); |         cy.visit('/testnet4/block/000000000066e8b6cc78a93f8989587f5819624bae2eb1c05f535cadded19f99'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('h2').invoke('text').should('equal', '11 transactions'); |         cy.get('h2').invoke('text').should('equal', '18 transactions'); | ||||||
|         cy.get('ul.pagination').first().children().should('have.length', 5); |         cy.get('ul.pagination').first().children().should('have.length', 5); | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('supports pagination on the block screen', () => { |       it('supports pagination on the block screen', () => { | ||||||
|         // 48 txs
 |         // 48 txs
 | ||||||
|         cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9'); |         cy.visit('/testnet4/block/000000000000006982d53f8273bdff21dafc380c292eabc669b5ab6d732311c3'); | ||||||
|  |         cy.get('.pagination').scrollIntoView({ offset: { top: 200, left: 0 } }); | ||||||
|         cy.waitForSkeletonGone(); |         cy.waitForSkeletonGone(); | ||||||
|         cy.get('.header-bg.box > a').invoke('text').then((text1) => { |         cy.get('.header-bg.box > a').invoke('text').then((text1) => { | ||||||
|           cy.get('.active + li').first().click().then(() => { |           cy.get('.active + li').first().click().then(() => { | ||||||
| @ -750,7 +750,7 @@ | |||||||
|     }, |     }, | ||||||
|     "backendInfo": { |     "backendInfo": { | ||||||
|       "hostname": "node205.tk7.mempool.space", |       "hostname": "node205.tk7.mempool.space", | ||||||
|       "version": "3.0.0-dev", |       "version": "3.1.0-dev", | ||||||
|       "gitCommit": "abbc8a134", |       "gitCommit": "abbc8a134", | ||||||
|       "lightning": false |       "lightning": false | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ Cypress.Commands.add('mockMempoolSocket', () => { | |||||||
|   mockWebSocket(); |   mockWebSocket(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "mainnet") => { | Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => { | ||||||
|   cy.get('.dropdown-toggle').click().then(() => { |   cy.get('.dropdown-toggle').click().then(() => { | ||||||
|     cy.get(`a.${network}`).click().then(() => { |     cy.get(`a.${network}`).click().then(() => { | ||||||
|       cy.waitForPageIdle(); |       cy.waitForPageIdle(); | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								frontend/cypress/support/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								frontend/cypress/support/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -5,6 +5,6 @@ declare namespace Cypress { | |||||||
|         waitForSkeletonGone(): Chainable<any> |         waitForSkeletonGone(): Chainable<any> | ||||||
|         waitForPageIdle(): Chainable<any> |         waitForPageIdle(): Chainable<any> | ||||||
|         mockMempoolSocket(): Chainable<any> |         mockMempoolSocket(): Chainable<any> | ||||||
|         changeNetwork(network: "testnet"|"signet"|"liquid"|"mainnet"): Chainable<any> |         changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any> | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -4,11 +4,14 @@ const { spawnSync } = require('child_process'); | |||||||
| const CONFIG_FILE_NAME = 'mempool-frontend-config.json'; | const CONFIG_FILE_NAME = 'mempool-frontend-config.json'; | ||||||
| const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js'; | const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js'; | ||||||
| const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js'; | const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js'; | ||||||
|  | const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js'; | ||||||
| 
 | 
 | ||||||
| let settings = []; | let settings = []; | ||||||
| let configContent = {}; | let configContent = {}; | ||||||
| let gitCommitHash = ''; | let gitCommitHash = ''; | ||||||
| let packetJsonVersion = ''; | let packetJsonVersion = ''; | ||||||
|  | let customConfig; | ||||||
|  | let customConfigContent; | ||||||
| 
 | 
 | ||||||
| try { | try { | ||||||
|   const rawConfig = fs.readFileSync(CONFIG_FILE_NAME); |   const rawConfig = fs.readFileSync(CONFIG_FILE_NAME); | ||||||
| @ -22,7 +25,18 @@ try { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const indexFilePath = configContent.BASE_MODULE ? 'src/index.' + configContent.BASE_MODULE + '.html' : 'src/index.mempool.html'; | if (configContent && configContent.CUSTOMIZATION) { | ||||||
|  |   try { | ||||||
|  |     customConfig = readConfig(configContent.CUSTOMIZATION); | ||||||
|  |     customConfigContent = JSON.parse(customConfig); | ||||||
|  |   } catch (e) { | ||||||
|  |     console.log(`failed to load customization config from ${configContent.CUSTOMIZATION}`); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const baseModuleName = configContent.BASE_MODULE || 'mempool'; | ||||||
|  | const customBuildName = (customConfigContent && customConfigContent.enterprise) ? ('.' + customConfigContent.enterprise) : ''; | ||||||
|  | const indexFilePath = 'src/index.' + baseModuleName + customBuildName + '.html'; | ||||||
| 
 | 
 | ||||||
| try { | try { | ||||||
|   fs.copyFileSync(indexFilePath, 'src/index.html'); |   fs.copyFileSync(indexFilePath, 'src/index.html'); | ||||||
| @ -109,6 +123,17 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate); | |||||||
| 
 | 
 | ||||||
| const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME); | const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME); | ||||||
| 
 | 
 | ||||||
|  | let customConfigJs = ''; | ||||||
|  | if (customConfig) { | ||||||
|  |   console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`); | ||||||
|  |   customConfigJs = `(function (window) {
 | ||||||
|  |     window.__env = window.__env || {}; | ||||||
|  |     window.__env.customize = ${customConfig}; | ||||||
|  |     }((typeof global !== 'undefined') ? global : this)); | ||||||
|  |   `;
 | ||||||
|  | } | ||||||
|  | writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs); | ||||||
|  | 
 | ||||||
| if (currentConfig && currentConfig === newConfig) { | if (currentConfig && currentConfig === newConfig) { | ||||||
|   console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`); |   console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`); | ||||||
|   return; |   return; | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
| { | { | ||||||
|   "TESTNET_ENABLED": false, |   "TESTNET_ENABLED": false, | ||||||
|  |   "TESTNET4_ENABLED": false, | ||||||
|   "SIGNET_ENABLED": false, |   "SIGNET_ENABLED": false, | ||||||
|   "LIQUID_ENABLED": false, |   "LIQUID_ENABLED": false, | ||||||
|   "LIQUID_TESTNET_ENABLED": false, |   "LIQUID_TESTNET_ENABLED": false, | ||||||
|  |   "MAINNET_ENABLED": true, | ||||||
|   "ITEMS_PER_PAGE": 10, |   "ITEMS_PER_PAGE": 10, | ||||||
|   "KEEP_BLOCKS_AMOUNT": 8, |   "KEEP_BLOCKS_AMOUNT": 8, | ||||||
|   "NGINX_PROTOCOL": "http", |   "NGINX_PROTOCOL": "http", | ||||||
| @ -11,6 +13,7 @@ | |||||||
|   "BLOCK_WEIGHT_UNITS": 4000000, |   "BLOCK_WEIGHT_UNITS": 4000000, | ||||||
|   "MEMPOOL_BLOCKS_AMOUNT": 8, |   "MEMPOOL_BLOCKS_AMOUNT": 8, | ||||||
|   "BASE_MODULE": "mempool", |   "BASE_MODULE": "mempool", | ||||||
|  |   "ROOT_NETWORK": "", | ||||||
|   "MEMPOOL_WEBSITE_URL": "https://mempool.space", |   "MEMPOOL_WEBSITE_URL": "https://mempool.space", | ||||||
|   "LIQUID_WEBSITE_URL": "https://liquid.network", |   "LIQUID_WEBSITE_URL": "https://liquid.network", | ||||||
|   "MINING_DASHBOARD": true, |   "MINING_DASHBOARD": true, | ||||||
| @ -22,5 +25,7 @@ | |||||||
|   "HISTORICAL_PRICE": true, |   "HISTORICAL_PRICE": true, | ||||||
|   "ADDITIONAL_CURRENCIES": false, |   "ADDITIONAL_CURRENCIES": false, | ||||||
|   "ACCELERATOR": false, |   "ACCELERATOR": false, | ||||||
|   "PUBLIC_ACCELERATIONS": false |   "ACCELERATOR_BUTTON": true, | ||||||
|  |   "PUBLIC_ACCELERATIONS": false, | ||||||
|  |   "SERVICES_API": "https://mempool.space/api/v1/services" | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										633
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										633
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "mempool-frontend", |   "name": "mempool-frontend", | ||||||
|   "version": "3.0.0-dev", |   "version": "3.1.0-dev", | ||||||
|   "description": "Bitcoin mempool visualizer and blockchain explorer backend", |   "description": "Bitcoin mempool visualizer and blockchain explorer backend", | ||||||
|   "license": "GNU Affero General Public License v3.0", |   "license": "GNU Affero General Public License v3.0", | ||||||
|   "homepage": "https://mempool.space", |   "homepage": "https://mempool.space", | ||||||
| @ -50,16 +50,16 @@ | |||||||
|     "dev:ssr": "npm run generate-config && ng run mempool:serve-ssr", |     "dev:ssr": "npm run generate-config && ng run mempool:serve-ssr", | ||||||
|     "serve:ssr": "npm run generate-config && node server.run.js", |     "serve:ssr": "npm run generate-config && node server.run.js", | ||||||
|     "build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts", |     "build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts", | ||||||
|     "config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config", |     "config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config", | ||||||
|     "config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config", |     "config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config", | ||||||
|     "prerender": "npm run ng -- run mempool:prerender", |     "prerender": "npm run ng -- run mempool:prerender", | ||||||
|     "cypress:open": "cypress open", |     "cypress:open": "cypress open", | ||||||
|     "cypress:run": "cypress run", |     "cypress:run": "cypress run", | ||||||
|     "cypress:run:record": "cypress run --record", |     "cypress:run:record": "cypress run --record", | ||||||
|     "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open", |     "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open", | ||||||
|     "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record", |     "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record", | ||||||
|     "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open", |     "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open", | ||||||
|     "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" |     "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular-devkit/build-angular": "^17.3.1", |     "@angular-devkit/build-angular": "^17.3.1", | ||||||
| @ -76,9 +76,9 @@ | |||||||
|     "@angular/router": "^17.3.1", |     "@angular/router": "^17.3.1", | ||||||
|     "@angular/ssr": "^17.3.1", |     "@angular/ssr": "^17.3.1", | ||||||
|     "@fortawesome/angular-fontawesome": "~0.14.1", |     "@fortawesome/angular-fontawesome": "~0.14.1", | ||||||
|     "@fortawesome/fontawesome-common-types": "~6.5.1", |     "@fortawesome/fontawesome-common-types": "~6.6.0", | ||||||
|     "@fortawesome/fontawesome-svg-core": "~6.5.1", |     "@fortawesome/fontawesome-svg-core": "~6.6.0", | ||||||
|     "@fortawesome/free-solid-svg-icons": "~6.5.1", |     "@fortawesome/free-solid-svg-icons": "~6.6.0", | ||||||
|     "@mempool/mempool.js": "2.3.0", |     "@mempool/mempool.js": "2.3.0", | ||||||
|     "@ng-bootstrap/ng-bootstrap": "^16.0.0", |     "@ng-bootstrap/ng-bootstrap": "^16.0.0", | ||||||
|     "@types/qrcode": "~1.5.0", |     "@types/qrcode": "~1.5.0", | ||||||
| @ -88,14 +88,14 @@ | |||||||
|     "domino": "^2.1.6", |     "domino": "^2.1.6", | ||||||
|     "echarts": "~5.5.0", |     "echarts": "~5.5.0", | ||||||
|     "lightweight-charts": "~3.8.0", |     "lightweight-charts": "~3.8.0", | ||||||
|     "ngx-echarts": "~17.1.0", |     "ngx-echarts": "~17.2.0", | ||||||
|     "ngx-infinite-scroll": "^17.0.0", |     "ngx-infinite-scroll": "^17.0.0", | ||||||
|     "qrcode": "1.5.1", |     "qrcode": "1.5.1", | ||||||
|     "rxjs": "~7.8.1", |     "rxjs": "~7.8.1", | ||||||
|     "esbuild": "^0.20.2", |     "esbuild": "^0.23.0", | ||||||
|     "tinyify": "^4.0.0", |     "tinyify": "^4.0.0", | ||||||
|     "tlite": "^0.1.9", |     "tlite": "^0.1.9", | ||||||
|     "tslib": "~2.6.0", |     "tslib": "~2.7.0", | ||||||
|     "zone.js": "~0.14.4" |     "zone.js": "~0.14.4" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
| @ -115,7 +115,7 @@ | |||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "@cypress/schematic": "^2.5.0", |     "@cypress/schematic": "^2.5.0", | ||||||
|     "@types/cypress": "^1.1.3", |     "@types/cypress": "^1.1.3", | ||||||
|     "cypress": "^13.7.0", |     "cypress": "^13.14.0", | ||||||
|     "cypress-fail-on-console-error": "~5.1.0", |     "cypress-fail-on-console-error": "~5.1.0", | ||||||
|     "cypress-wait-until": "^2.0.1", |     "cypress-wait-until": "^2.0.1", | ||||||
|     "mock-socket": "~9.3.1", |     "mock-socket": "~9.3.1", | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ PROXY_CONFIG = [ | |||||||
|         '/api/**', '!/api/v1/ws', |         '/api/**', '!/api/v1/ws', | ||||||
|         '!/liquid', '!/liquid/**', '!/liquid/', |         '!/liquid', '!/liquid/**', '!/liquid/', | ||||||
|         '!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/', |         '!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/', | ||||||
|         '/testnet/api/**', '/signet/api/**' |         '/testnet/api/**', '/signet/api/**', '/testnet4/api/**' | ||||||
|         ], |         ], | ||||||
|         target: "https://mempool.space", |         target: "https://mempool.space", | ||||||
|         ws: true, |         ws: true, | ||||||
|  | |||||||
| @ -78,6 +78,18 @@ PROXY_CONFIG.push(...[ | |||||||
|         "^/testnet": "" |         "^/testnet": "" | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   /* Optional proxy to route dev to official acceleration services | ||||||
|  |   { | ||||||
|  |     context: ['/api/v1/services/accelerator/**'], | ||||||
|  |     target: `https://mempool.space/api/v1/services/accelerator/`, | ||||||
|  |     secure: false, | ||||||
|  |     changeOrigin: true, | ||||||
|  |     proxyTimeout: 30000, | ||||||
|  |     pathRewrite: { | ||||||
|  |       "^/api/v1/services/accelerator": "" | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   */ | ||||||
|   { |   { | ||||||
|     context: ['/api/v1/services/**'], |     context: ['/api/v1/services/**'], | ||||||
|     target: `http://localhost:9000`, |     target: `http://localhost:9000`, | ||||||
|  | |||||||
| @ -7,6 +7,9 @@ import { MempoolBlockViewComponent } from './components/mempool-block-view/mempo | |||||||
| import { ClockComponent } from './components/clock/clock.component'; | import { ClockComponent } from './components/clock/clock.component'; | ||||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||||
| import { AddressGroupComponent } from './components/address-group/address-group.component'; | import { AddressGroupComponent } from './components/address-group/address-group.component'; | ||||||
|  | import { TrackerComponent } from './components/tracker/tracker.component'; | ||||||
|  | import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; | ||||||
|  | import { TrackerGuard } from './route-guards'; | ||||||
| 
 | 
 | ||||||
| const browserWindow = window || {}; | const browserWindow = window || {}; | ||||||
| // @ts-ignore
 | // @ts-ignore
 | ||||||
| @ -51,6 +54,44 @@ let routes: Routes = [ | |||||||
|       }, |       }, | ||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: 'testnet4', | ||||||
|  |     children: [ | ||||||
|  |       { | ||||||
|  |         path: '', | ||||||
|  |         pathMatch: 'full', | ||||||
|  |         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||||
|  |         data: { preload: true }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         path: '', | ||||||
|  |         loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), | ||||||
|  |         data: { preload: true }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         path: 'wallet', | ||||||
|  |         children: [], | ||||||
|  |         component: AddressGroupComponent, | ||||||
|  |         data: { | ||||||
|  |           networkSpecific: true, | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         path: 'status', | ||||||
|  |         data: { networks: ['bitcoin', 'liquid'] }, | ||||||
|  |         component: StatusViewComponent | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         path: '', | ||||||
|  |         loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||||
|  |         data: { preload: true }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         path: '**', | ||||||
|  |         redirectTo: '/testnet4' | ||||||
|  |       }, | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     path: 'signet', |     path: 'signet', | ||||||
|     children: [ |     children: [ | ||||||
| @ -100,6 +141,12 @@ let routes: Routes = [ | |||||||
|     loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), |     loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||||
|     data: { preload: true }, |     data: { preload: true }, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: 'tx', | ||||||
|  |     canMatch: [TrackerGuard], | ||||||
|  |     runGuardsAndResolvers: 'always', | ||||||
|  |     loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     path: '', |     path: '', | ||||||
|     loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), |     loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), | ||||||
| @ -124,6 +171,10 @@ let routes: Routes = [ | |||||||
|         path: 'testnet', |         path: 'testnet', | ||||||
|         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) |         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||||
|       }, |       }, | ||||||
|  |       { | ||||||
|  |         path: 'testnet4', | ||||||
|  |         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         path: 'signet', |         path: 'signet', | ||||||
|         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) |         loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) | ||||||
| @ -164,10 +215,6 @@ let routes: Routes = [ | |||||||
|     loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), |     loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), | ||||||
|     data: { preload: true }, |     data: { preload: true }, | ||||||
|   }, |   }, | ||||||
|   { |  | ||||||
|     path: '**', |  | ||||||
|     redirectTo: '' |  | ||||||
|   }, |  | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | ||||||
| @ -252,13 +299,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { | |||||||
|       loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), |       loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), | ||||||
|       data: { preload: true }, |       data: { preload: true }, | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|       path: '**', |  | ||||||
|       redirectTo: '' |  | ||||||
|     }, |  | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | if (!window['isMempoolSpaceBuild']) { | ||||||
|  |   routes.push({ | ||||||
|  |     path: '**', | ||||||
|  |     redirectTo: '' | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   imports: [RouterModule.forRoot(routes, { |   imports: [RouterModule.forRoot(routes, { | ||||||
|     initialNavigation: 'enabledBlocking', |     initialNavigation: 'enabledBlocking', | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| export const mempoolFeeColors = [ | export const defaultMempoolFeeColors = [ | ||||||
|   '557d00', |   '557d00', | ||||||
|   '5d7d01', |   '5d7d01', | ||||||
|   '637d02', |   '637d02', | ||||||
| @ -39,6 +39,47 @@ export const mempoolFeeColors = [ | |||||||
|   'ae005b', |   'ae005b', | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | export const contrastMempoolFeeColors = [ | ||||||
|  |   '0082e6', | ||||||
|  |   '0984df', | ||||||
|  |   '1285d9', | ||||||
|  |   '1a87d2', | ||||||
|  |   '2388cb', | ||||||
|  |   '2c8ac5', | ||||||
|  |   '358bbe', | ||||||
|  |   '3e8db7', | ||||||
|  |   '468eb0', | ||||||
|  |   '4f90aa', | ||||||
|  |   '5892a3', | ||||||
|  |   '61939c', | ||||||
|  |   '6a9596', | ||||||
|  |   '72968f', | ||||||
|  |   '7b9888', | ||||||
|  |   '849982', | ||||||
|  |   '8d9b7b', | ||||||
|  |   '959c74', | ||||||
|  |   '9e9e6e', | ||||||
|  |   'a79f67', | ||||||
|  |   'b0a160', | ||||||
|  |   'b9a35a', | ||||||
|  |   'c1a453', | ||||||
|  |   'caa64c', | ||||||
|  |   'd3a745', | ||||||
|  |   'dca93f', | ||||||
|  |   'e5aa38', | ||||||
|  |   'edac31', | ||||||
|  |   'f6ad2b', | ||||||
|  |   'ffaf24', | ||||||
|  |   'ffb01e', | ||||||
|  |   'ffb118', | ||||||
|  |   'ffb212', | ||||||
|  |   'ffb30c', | ||||||
|  |   'ffb406', | ||||||
|  |   'ffb500', | ||||||
|  |   'ffb600', | ||||||
|  |   'ffb700', | ||||||
|  |  ]; | ||||||
|  | 
 | ||||||
| export const chartColors = [ | export const chartColors = [ | ||||||
|   "#D81B60", |   "#D81B60", | ||||||
|   "#8E24AA", |   "#8E24AA", | ||||||
| @ -110,7 +151,7 @@ export const languages: Language[] = [ | |||||||
|    { code: 'fr', name: 'Français' },        // French
 |    { code: 'fr', name: 'Français' },        // French
 | ||||||
| // { code: 'gl', name: 'Galego' },          // Galician
 | // { code: 'gl', name: 'Galego' },          // Galician
 | ||||||
|    { code: 'ko', name: '한국어' },          // Korean
 |    { code: 'ko', name: '한국어' },          // Korean
 | ||||||
| // { code: 'hr', name: 'Hrvatski' },        // Croatian
 |    { code: 'hr', name: 'Hrvatski' },        // Croatian
 | ||||||
| // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
 | // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
 | ||||||
|    { code: 'hi', name: 'हिन्दी' },             // Hindi
 |    { code: 'hi', name: 'हिन्दी' },             // Hindi
 | ||||||
|    { code: 'ne', name: 'नेपाली' },            // Nepalese
 |    { code: 'ne', name: 'नेपाली' },            // Nepalese
 | ||||||
| @ -148,22 +189,22 @@ export const specialBlocks = { | |||||||
|   '0': { |   '0': { | ||||||
|     labelEvent: 'Genesis', |     labelEvent: 'Genesis', | ||||||
|     labelEventCompleted: 'The Genesis of Bitcoin', |     labelEventCompleted: 'The Genesis of Bitcoin', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '210000': { |   '210000': { | ||||||
|     labelEvent: 'Bitcoin\'s 1st Halving', |     labelEvent: 'Bitcoin\'s 1st Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '420000': { |   '420000': { | ||||||
|     labelEvent: 'Bitcoin\'s 2nd Halving', |     labelEvent: 'Bitcoin\'s 2nd Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '630000': { |   '630000': { | ||||||
|     labelEvent: 'Bitcoin\'s 3rd Halving', |     labelEvent: 'Bitcoin\'s 3rd Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '709632': { |   '709632': { | ||||||
|     labelEvent: 'Taproot 🌱 activation', |     labelEvent: 'Taproot 🌱 activation', | ||||||
| @ -173,62 +214,62 @@ export const specialBlocks = { | |||||||
|   '840000': { |   '840000': { | ||||||
|     labelEvent: 'Bitcoin\'s 4th Halving', |     labelEvent: 'Bitcoin\'s 4th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '1050000': { |   '1050000': { | ||||||
|     labelEvent: 'Bitcoin\'s 5th Halving', |     labelEvent: 'Bitcoin\'s 5th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '1260000': { |   '1260000': { | ||||||
|     labelEvent: 'Bitcoin\'s 6th Halving', |     labelEvent: 'Bitcoin\'s 6th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '1470000': { |   '1470000': { | ||||||
|     labelEvent: 'Bitcoin\'s 7th Halving', |     labelEvent: 'Bitcoin\'s 7th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '1680000': { |   '1680000': { | ||||||
|     labelEvent: 'Bitcoin\'s 8th Halving', |     labelEvent: 'Bitcoin\'s 8th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '1890000': { |   '1890000': { | ||||||
|     labelEvent: 'Bitcoin\'s 9th Halving', |     labelEvent: 'Bitcoin\'s 9th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '2100000': { |   '2100000': { | ||||||
|     labelEvent: 'Bitcoin\'s 10th Halving', |     labelEvent: 'Bitcoin\'s 10th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '2310000': { |   '2310000': { | ||||||
|     labelEvent: 'Bitcoin\'s 11th Halving', |     labelEvent: 'Bitcoin\'s 11th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '2520000': { |   '2520000': { | ||||||
|     labelEvent: 'Bitcoin\'s 12th Halving', |     labelEvent: 'Bitcoin\'s 12th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '2730000': { |   '2730000': { | ||||||
|     labelEvent: 'Bitcoin\'s 13th Halving', |     labelEvent: 'Bitcoin\'s 13th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '2940000': { |   '2940000': { | ||||||
|     labelEvent: 'Bitcoin\'s 14th Halving', |     labelEvent: 'Bitcoin\'s 14th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   }, |   }, | ||||||
|   '3150000': { |   '3150000': { | ||||||
|     labelEvent: 'Bitcoin\'s 15th Halving', |     labelEvent: 'Bitcoin\'s 15th Halving', | ||||||
|     labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', |     labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', | ||||||
|     networks: ['mainnet', 'testnet'], |     networks: ['mainnet', 'testnet', 'testnet4'], | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ import { PriceService } from './services/price.service'; | |||||||
| import { EnterpriseService } from './services/enterprise.service'; | import { EnterpriseService } from './services/enterprise.service'; | ||||||
| import { WebsocketService } from './services/websocket.service'; | import { WebsocketService } from './services/websocket.service'; | ||||||
| import { AudioService } from './services/audio.service'; | import { AudioService } from './services/audio.service'; | ||||||
|  | import { PreloadService } from './services/preload.service'; | ||||||
| import { SeoService } from './services/seo.service'; | import { SeoService } from './services/seo.service'; | ||||||
| import { OpenGraphService } from './services/opengraph.service'; | import { OpenGraphService } from './services/opengraph.service'; | ||||||
| import { ZoneService } from './services/zone-shim.service'; | import { ZoneService } from './services/zone-shim.service'; | ||||||
| @ -19,12 +20,14 @@ import { SharedModule } from './shared/shared.module'; | |||||||
| import { StorageService } from './services/storage.service'; | import { StorageService } from './services/storage.service'; | ||||||
| import { HttpCacheInterceptor } from './services/http-cache.interceptor'; | import { HttpCacheInterceptor } from './services/http-cache.interceptor'; | ||||||
| import { LanguageService } from './services/language.service'; | import { LanguageService } from './services/language.service'; | ||||||
|  | import { ThemeService } from './services/theme.service'; | ||||||
| import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; | import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; | ||||||
| import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; | import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; | ||||||
| import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; | import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; | ||||||
| import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; | import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; | ||||||
| import { AppPreloadingStrategy } from './app.preloading-strategy'; | import { AppPreloadingStrategy } from './app.preloading-strategy'; | ||||||
| import { ServicesApiServices } from './services/services-api.service'; | import { ServicesApiServices } from './services/services-api.service'; | ||||||
|  | import { DatePipe } from '@angular/common'; | ||||||
| 
 | 
 | ||||||
| const providers = [ | const providers = [ | ||||||
|   ElectrsApiService, |   ElectrsApiService, | ||||||
| @ -38,12 +41,15 @@ const providers = [ | |||||||
|   StorageService, |   StorageService, | ||||||
|   EnterpriseService, |   EnterpriseService, | ||||||
|   LanguageService, |   LanguageService, | ||||||
|  |   ThemeService, | ||||||
|   ShortenStringPipe, |   ShortenStringPipe, | ||||||
|   FiatShortenerPipe, |   FiatShortenerPipe, | ||||||
|   FiatCurrencyPipe, |   FiatCurrencyPipe, | ||||||
|   CapAddressPipe, |   CapAddressPipe, | ||||||
|  |   DatePipe, | ||||||
|   AppPreloadingStrategy, |   AppPreloadingStrategy, | ||||||
|   ServicesApiServices, |   ServicesApiServices, | ||||||
|  |   PreloadService, | ||||||
|   { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }, |   { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }, | ||||||
|   { provide: ZONE_SERVICE, useClass: ZoneService }, |   { provide: ZONE_SERVICE, useClass: ZoneService }, | ||||||
| ]; | ]; | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { Transaction, Vin } from './interfaces/electrs.interface'; | import { Transaction, Vin } from './interfaces/electrs.interface'; | ||||||
|  | import { Hash } from './shared/sha256'; | ||||||
| 
 | 
 | ||||||
| const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
 | const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
 | ||||||
| const P2SH_P2WSH_COST  = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
 | const P2SH_P2WSH_COST  = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
 | ||||||
| @ -70,19 +71,24 @@ export function calcSegwitFeeGains(tx: Transaction) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (isP2tr) { |     if (isP2tr) { | ||||||
|       if (vin.witness.length === 1) { |       // every valid taproot input has at least one witness item, however transactions
 | ||||||
|         // key path spend
 |       // created before taproot activation don't need to have any witness data
 | ||||||
|         // we don't know if this was a multisig or single sig (the goal of taproot :)),
 |       // (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
 | ||||||
|         // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
 |       if (vin.witness?.length) { | ||||||
|         // the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
 |         if (vin.witness.length === 1) { | ||||||
|         // the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
 |           // key path spend
 | ||||||
|         realizedTaprootGains += 42; |           // we don't know if this was a multisig or single sig (the goal of taproot :)),
 | ||||||
|       } else { |           // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
 | ||||||
|         // script path spend
 |           // the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
 | ||||||
|         // complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
 |           // the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
 | ||||||
|         // because only the hash of the alternative spending path has the be in the witness data, not the entire script,
 |           realizedTaprootGains += 42; | ||||||
|         // but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
 |         } else { | ||||||
|         // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 |           // script path spend
 | ||||||
|  |           // complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
 | ||||||
|  |           // because only the hash of the alternative spending path has the be in the witness data, not the entire script,
 | ||||||
|  |           // but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
 | ||||||
|  |           // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; |       const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; | ||||||
| @ -129,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const opN = ops.pop(); |   const opN = ops.pop(); | ||||||
|   if (!opN.startsWith('OP_PUSHNUM_')) { |   if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const n = parseInt(opN.match(/[0-9]+/)[0], 10); |   const n = parseInt(opN.match(/[0-9]+/)[0], 10); | ||||||
| @ -146,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   const opM = ops.pop(); |   const opM = ops.pop(); | ||||||
|   if (!opM.startsWith('OP_PUSHNUM_')) { |   if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const m = parseInt(opM.match(/[0-9]+/)[0], 10); |   const m = parseInt(opM.match(/[0-9]+/)[0], 10); | ||||||
| @ -266,6 +272,11 @@ const featureActivation = { | |||||||
|     segwit: 872730, |     segwit: 872730, | ||||||
|     taproot: 2032291, |     taproot: 2032291, | ||||||
|   }, |   }, | ||||||
|  |   testnet4: { | ||||||
|  |     rbf: 0, | ||||||
|  |     segwit: 0, | ||||||
|  |     taproot: 0, | ||||||
|  |   }, | ||||||
|   signet: { |   signet: { | ||||||
|     rbf: 0, |     rbf: 0, | ||||||
|     segwit: 0, |     segwit: 0, | ||||||
| @ -287,8 +298,8 @@ export async function calcScriptHash$(script: string): Promise<string> { | |||||||
|     throw new Error('script is not a valid hex string'); |     throw new Error('script is not a valid hex string'); | ||||||
|   } |   } | ||||||
|   const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16))); |   const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16))); | ||||||
|   const hashBuffer = await crypto.subtle.digest('SHA-256', buf); |   const hash = new Hash().update(buf).digest(); | ||||||
|   const hashArray = Array.from(new Uint8Array(hashBuffer)); |   const hashArray = Array.from(new Uint8Array(hash)); | ||||||
|   return hashArray |   return hashArray | ||||||
|     .map((bytes) => bytes.toString(16).padStart(2, '0')) |     .map((bytes) => bytes.toString(16).padStart(2, '0')) | ||||||
|     .join(''); |     .join(''); | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .become-sponsor { | .become-sponsor { | ||||||
|   background-color: #1d1f31; |   background-color: var(--bg); | ||||||
|   border-radius: 16px; |   border-radius: 16px; | ||||||
|   padding: 12px 20px; |   padding: 12px 20px; | ||||||
|   width: 400px; |   width: 400px; | ||||||
|  | |||||||
| @ -53,13 +53,26 @@ | |||||||
|         <span>Spiral</span> |         <span>Spiral</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://foundrydigital.com/" target="_blank" title="Foundry"> |       <a href="https://foundrydigital.com/" target="_blank" title="Foundry"> | ||||||
|         <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image"> |         <svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image"> | ||||||
|           <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |           <defs> | ||||||
|             <g transform="translate(-186.000000, -2316.000000)"> |             <style> | ||||||
|               <g transform="translate(186.000000, 2316.000000)"> |               .d { | ||||||
|                 <rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect> |                 fill: #fff; | ||||||
|                 <path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path> |               } | ||||||
|               </g> | 
 | ||||||
|  |               .e { | ||||||
|  |                 fill: #ff8200; | ||||||
|  |               } | ||||||
|  |             </style> | ||||||
|  |           </defs> | ||||||
|  |           <g id="c" data-name="b"> | ||||||
|  |             <circle class="e" cx="24" cy="32" r="8" /> | ||||||
|  |             <circle class="e" cx="24" cy="56" r="8" /> | ||||||
|  |             <circle class="e" cx="8" cy="68" r="8" /> | ||||||
|  |             <g> | ||||||
|  |               <circle class="d" cx="24" cy="8" r="8" /> | ||||||
|  |               <circle class="d" cx="8" cy="20" r="8" /> | ||||||
|  |               <circle class="d" cx="8" cy="44" r="8" /> | ||||||
|             </g> |             </g> | ||||||
|           </g> |           </g> | ||||||
|         </svg> |         </svg> | ||||||
| @ -112,17 +125,14 @@ | |||||||
|         <span>Blockstream</span> |         <span>Blockstream</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://unchained.com/" target="_blank" title="Unchained"> |       <a href="https://unchained.com/" target="_blank" title="Unchained"> | ||||||
|         <svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg> |         <svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image"> | ||||||
|  |           <defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/> | ||||||
|  |         </svg> | ||||||
|         <span>Unchained</span> |         <span>Unchained</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://gemini.com/" target="_blank" title="Gemini"> |       <a href="https://bitkey.world/" target="_blank" title="Bitkey"> | ||||||
|         <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image"> |         <img class="image" src="/resources/profile/bitkey.svg" /> | ||||||
|           <rect style="fill: black" width="360" height="360" /> |         <span>Bitkey</span> | ||||||
|           <g transform="matrix(0.62 0 0 0.62 180 180)"> |  | ||||||
|             <path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" /> |  | ||||||
|           </g> |  | ||||||
|         </svg> |  | ||||||
|         <span>Gemini</span> |  | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin"> |       <a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin"> | ||||||
|         <svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg"> |         <svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg"> | ||||||
| @ -137,7 +147,7 @@ | |||||||
|         <span>Bull Bitcoin</span> |         <span>Bull Bitcoin</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://exodus.com/" target="_blank" title="Exodus"> |       <a href="https://exodus.com/" target="_blank" title="Exodus"> | ||||||
|         <svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"> |         <svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image"> | ||||||
|           <circle cx="250" cy="250" r="250" fill="#1F2033"/> |           <circle cx="250" cy="250" r="250" fill="#1F2033"/> | ||||||
|           <g clip-path="url(#clip0_2_14)"> |           <g clip-path="url(#clip0_2_14)"> | ||||||
|             <path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/> |             <path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/> | ||||||
| @ -178,6 +188,19 @@ | |||||||
|         </svg> |         </svg> | ||||||
|         <span>Exodus</span> |         <span>Exodus</span> | ||||||
|       </a> |       </a> | ||||||
|  |       <a href="https://gemini.com/" target="_blank" title="Gemini"> | ||||||
|  |         <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image"> | ||||||
|  |           <rect style="fill: black" width="360" height="360" /> | ||||||
|  |           <g transform="matrix(0.62 0 0 0.62 180 180)"> | ||||||
|  |             <path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" /> | ||||||
|  |           </g> | ||||||
|  |         </svg> | ||||||
|  |         <span>Gemini</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="https://leather.io/" target="_blank" title="Leather"> | ||||||
|  |         <img class="image" src="/resources/profile/leather.svg" /> | ||||||
|  |         <span>Leather</span> | ||||||
|  |       </a> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
| @ -188,8 +211,8 @@ | |||||||
|         <div class="wrapper"> |         <div class="wrapper"> | ||||||
|           <ng-container> |           <ng-container> | ||||||
|             <ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> |             <ng-template ngFor let-sponsor [ngForOf]="profiles.whales"> | ||||||
|               <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> |               <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> | ||||||
|                 <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> |                 <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|               </a> |               </a> | ||||||
|             </ng-template> |             </ng-template> | ||||||
|           </ng-container> |           </ng-container> | ||||||
| @ -200,8 +223,8 @@ | |||||||
|         <h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3> |         <h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3> | ||||||
|         <div class="wrapper"> |         <div class="wrapper"> | ||||||
|           <ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> |           <ng-template ngFor let-sponsor [ngForOf]="profiles.chads"> | ||||||
|             <a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> |             <a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username"> | ||||||
|               <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> |               <img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|             </a> |             </a> | ||||||
|           </ng-template> |           </ng-template> | ||||||
|         </div> |         </div> | ||||||
| @ -213,8 +236,8 @@ | |||||||
|     <h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3> |     <h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3> | ||||||
|     <div class="wrapper"> |     <div class="wrapper"> | ||||||
|       <ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors"> |       <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"> |         <a *ngFor="let ogSponsor of ogs" [href]="'https://x.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'"/> |           <img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|         </a> |         </a> | ||||||
|       </ng-container> |       </ng-container> | ||||||
|     </div> |     </div> | ||||||
| @ -259,22 +282,10 @@ | |||||||
|         <img class="image" src="/resources/profile/bisq_network.png" /> |         <img class="image" src="/resources/profile/bisq_network.png" /> | ||||||
|         <span>Bisq</span> |         <span>Bisq</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> |  | ||||||
|         <img class="image" src="/resources/profile/bluewallet.png" /> |  | ||||||
|         <span>BlueWallet</span> |  | ||||||
|       </a> |  | ||||||
|       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> |  | ||||||
|         <img class="image" src="/resources/profile/muun.png" /> |  | ||||||
|         <span>Muun</span> |  | ||||||
|       </a> |  | ||||||
|       <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet"> |       <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet"> | ||||||
|         <img class="image" src="/resources/profile/electrum.png" /> |         <img class="image" src="/resources/profile/electrum.png" /> | ||||||
|         <span>Electrum</span> |         <span>Electrum</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet"> |  | ||||||
|         <img class="image" src="/resources/profile/specter.png" /> |  | ||||||
|         <span>Specter</span> |  | ||||||
|       </a> |  | ||||||
|       <a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet"> |       <a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet"> | ||||||
|         <img class="image" src="/resources/profile/sparrow.png" /> |         <img class="image" src="/resources/profile/sparrow.png" /> | ||||||
|         <span>Sparrow</span> |         <span>Sparrow</span> | ||||||
| @ -283,21 +294,37 @@ | |||||||
|         <img class="image not-rounded" src="/resources/profile/phoenix.svg" /> |         <img class="image not-rounded" src="/resources/profile/phoenix.svg" /> | ||||||
|         <span>Phoenix</span> |         <span>Phoenix</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> |       <a href="http://github.com/COLDCARD" target="_blank" title="COLDCARD"> | ||||||
|         <img class="image" src="/resources/profile/lnbits.svg" /> |         <img class="image coldcard" src="/resources/profile/coldcard.png" /> | ||||||
|         <span>LNBits</span> |         <span>COLDCARD</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet"> |       <a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> | ||||||
|         <img class="image" src="/resources/profile/mercury.svg" /> |         <img class="image" src="/resources/profile/zeus.png" /> | ||||||
|         <span>Mercury</span> |         <span>ZEUS</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny"> | ||||||
|  |         <img class="image not-rounded" src="/resources/profile/mutiny.svg" /> | ||||||
|  |         <span>Mutiny</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet"> |       <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet"> | ||||||
|         <img class="image" src="/resources/profile/blixt.png" /> |         <img class="image" src="/resources/profile/blixt.png" /> | ||||||
|         <span>Blixt</span> |         <span>Blixt</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> |       <a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck"> | ||||||
|         <img class="image" src="/resources/profile/zeus.png" /> |         <img class="image" src="/resources/profile/nunchuk.svg" /> | ||||||
|         <span>ZEUS</span> |         <span>Nunchuk</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> | ||||||
|  |         <img class="image" src="/resources/profile/bluewallet.png" /> | ||||||
|  |         <span>BlueWallet</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="https://github.com/BoltzExchange" target="_blank" title="Boltz"> | ||||||
|  |         <img class="image" src="/resources/profile/boltz.svg" /> | ||||||
|  |         <span>Boltz</span> | ||||||
|  |       </a> | ||||||
|  |       <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> | ||||||
|  |         <img class="image" src="/resources/profile/lnbits.svg" /> | ||||||
|  |         <span>LNBits</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet"> |       <a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet"> | ||||||
|         <img class="image" src="/resources/profile/marina.svg" /> |         <img class="image" src="/resources/profile/marina.svg" /> | ||||||
| @ -307,13 +334,9 @@ | |||||||
|         <img class="image" src="/resources/profile/schildbach.svg" /> |         <img class="image" src="/resources/profile/schildbach.svg" /> | ||||||
|         <span>Schildbach</span> |         <span>Schildbach</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck"> |       <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet"> | ||||||
|         <img class="image" src="/resources/profile/nunchuk.svg" /> |         <img class="image" src="/resources/profile/specter.png" /> | ||||||
|         <span>Nunchuk</span> |         <span>Specter</span> | ||||||
|       </a> |  | ||||||
|       <a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s"> |  | ||||||
|         <img class="image" src="/resources/profile/bitcoin-s.svg" /> |  | ||||||
|         <span>bitcoin-s</span> |  | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/EdgeApp" target="_blank" title="Edge"> |       <a href="https://github.com/EdgeApp" target="_blank" title="Edge"> | ||||||
|         <img class="image not-rounded" src="/resources/profile/edge.svg" /> |         <img class="image not-rounded" src="/resources/profile/edge.svg" /> | ||||||
| @ -323,13 +346,13 @@ | |||||||
|         <img class="image" src="/resources/profile/galoy.svg" /> |         <img class="image" src="/resources/profile/galoy.svg" /> | ||||||
|         <span>Galoy</span> |         <span>Galoy</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/BoltzExchange" target="_blank" title="Boltz"> |       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> | ||||||
|         <img class="image" src="/resources/profile/boltz.svg" /> |         <img class="image" src="/resources/profile/muun.png" /> | ||||||
|         <span>Boltz</span> |         <span>Muun</span> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny"> |       <a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s"> | ||||||
|         <img class="image not-rounded" src="/resources/profile/mutiny.svg" /> |         <img class="image" src="/resources/profile/bitcoin-s.svg" /> | ||||||
|         <span>Mutiny</span> |         <span>bitcoin-s</span> | ||||||
|       </a> |       </a> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @ -343,8 +366,8 @@ | |||||||
|       <a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance"> |       <a href="https://opencrypto.org/" title="Coppa - Crypto Open Patent Alliance"> | ||||||
|         <img class="copa" src="/resources/profile/copa.png" /> |         <img class="copa" src="/resources/profile/copa.png" /> | ||||||
|       </a> |       </a> | ||||||
|       <a href="https://bisq.network/" title="Bisq Network"> |       <a href="https://bitcoin.gob.sv" title="Oficina Nacional del Bitcoin"> | ||||||
|         <img class="bisq" src="/resources/profile/bisq.svg" /> |         <img class="sv" src="/resources/profile/onbtc-full.svg" /> | ||||||
|       </a> |       </a> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| @ -354,8 +377,8 @@ | |||||||
|       <h3 i18n="about.translators">Project Translators</h3> |       <h3 i18n="about.translators">Project Translators</h3> | ||||||
|       <div class="wrapper"> |       <div class="wrapper"> | ||||||
|         <ng-template ngFor let-translator [ngForOf]="translators"> |         <ng-template ngFor let-translator [ngForOf]="translators"> | ||||||
|           <a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key"> |           <a [href]="'https://x.com/' + translator.value" target="_blank" [title]="translator.key"> | ||||||
|             <img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> |             <img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|           </a> |           </a> | ||||||
|         </ng-template> |         </ng-template> | ||||||
|       </div> |       </div> | ||||||
| @ -369,7 +392,7 @@ | |||||||
|       <div class="wrapper"> |       <div class="wrapper"> | ||||||
|         <ng-template ngFor let-contributor [ngForOf]="contributors.regular"> |         <ng-template ngFor let-contributor [ngForOf]="contributors.regular"> | ||||||
|           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> |           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> | ||||||
|             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> |             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|             <span>{{ contributor.name }}</span> |             <span>{{ contributor.name }}</span> | ||||||
|           </a> |           </a> | ||||||
|         </ng-template> |         </ng-template> | ||||||
| @ -380,8 +403,8 @@ | |||||||
|       <h3 i18n="about.project_members">Project Members</h3> |       <h3 i18n="about.project_members">Project Members</h3> | ||||||
|       <div class="wrapper"> |       <div class="wrapper"> | ||||||
|         <ng-template ngFor let-contributor [ngForOf]="contributors.core"> |         <ng-template ngFor let-contributor [ngForOf]="contributors.core"> | ||||||
|           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name"> |           <a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'"> | ||||||
|             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/> |             <img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/> | ||||||
|             <span>{{ contributor.name }}</span> |             <span>{{ contributor.name }}</span> | ||||||
|           </a> |           </a> | ||||||
|         </ng-template> |         </ng-template> | ||||||
| @ -392,11 +415,11 @@ | |||||||
|   <div class="maintainers" id="project-maintainers"> |   <div class="maintainers" id="project-maintainers"> | ||||||
|     <h3 i18n="about.maintainers">Project Maintainers</h3> |     <h3 i18n="about.maintainers">Project Maintainers</h3> | ||||||
|     <div class="wrapper"> |     <div class="wrapper"> | ||||||
|         <a href="https://twitter.com/softsimon_" target="_blank" title="softsimon"> |         <a href="https://x.com/softsimon_" target="_blank" title="softsimon"> | ||||||
|           <img class="image" src="/resources/profile/softsimon.jpg" /> |           <img class="image" src="/resources/profile/softsimon.jpg" /> | ||||||
|           <span>softsimon</span> |           <span>softsimon</span> | ||||||
|         </a> |         </a> | ||||||
|         <a href="https://twitter.com/wiz" target="_blank" title="wiz"> |         <a href="https://x.com/wiz" target="_blank" title="wiz"> | ||||||
|           <img class="image" src="/resources/profile/wiz.png" /> |           <img class="image" src="/resources/profile/wiz.png" /> | ||||||
|           <span>wiz</span> |           <span>wiz</span> | ||||||
|         </a> |         </a> | ||||||
| @ -422,7 +445,7 @@ | |||||||
|       Trademark Notice<br> |       Trademark Notice<br> | ||||||
|     </div> |     </div> | ||||||
|     <p> |     <p> | ||||||
|       The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. |       The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. | ||||||
|     </p> |     </p> | ||||||
|     <p> |     <p> | ||||||
|       While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>. |       While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>. | ||||||
|  | |||||||
| @ -10,14 +10,9 @@ | |||||||
|     margin: 25px; |     margin: 25px; | ||||||
|     line-height: 32px; |     line-height: 32px; | ||||||
|   } |   } | ||||||
|   .unknown { |  | ||||||
|     border: 1px solid #b4b4b4; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   .image.not-rounded { |   .image.not-rounded { | ||||||
|     border-radius: 0; |     border-radius: 0; | ||||||
|     width: 60px; |  | ||||||
|     height: 60px; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   .intro { |   .intro { | ||||||
| @ -129,8 +124,9 @@ | |||||||
|       position: relative; |       position: relative; | ||||||
|       width: 300px; |       width: 300px; | ||||||
|     } |     } | ||||||
|     .bisq { |     .sv { | ||||||
|       top: 3px; |       height: 85px; | ||||||
|  |       width: auto; | ||||||
|       position: relative; |       position: relative; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -158,6 +154,11 @@ | |||||||
|         } |         } | ||||||
|         img, svg { |         img, svg { | ||||||
|           margin: 40px 29px 10px; |           margin: 40px 29px 10px; | ||||||
|  |           &.image.coldcard { | ||||||
|  |             border-radius: 0; | ||||||
|  |             height: auto; | ||||||
|  |             margin: 20px 29px 20px; | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -177,6 +178,10 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   #project-members a.project-member-avatar img { | ||||||
|  |     margin: 40px 20px 10px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .copyright { |   .copyright { | ||||||
|     text-align: left; |     text-align: left; | ||||||
|     max-width: 620px; |     max-width: 620px; | ||||||
| @ -246,3 +251,12 @@ | |||||||
|   width: 64px; |   width: 64px; | ||||||
|   height: 64px; |   height: 64px; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .enterprise-sponsor { | ||||||
|  |   .wrapper { | ||||||
|  |     display: flex; | ||||||
|  |     flex-wrap: wrap; | ||||||
|  |     justify-content: center; | ||||||
|  |     max-width: 800px;  | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -0,0 +1,600 @@ | |||||||
|  | <div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor> | ||||||
|  |   @if (accelerateError) { | ||||||
|  |     <div class="row mb-1 text-center"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="row text-center mt-1"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="d-flex flex-row justify-content-center align-items-center"> | ||||||
|  |           <span i18n="accelerator.error-failed-to-accelerate">We were not able to accelerate this transaction. Please try again later.</span> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <hr> | ||||||
|  |     <div class="row mt-2 mb-2 text-center"> | ||||||
|  |       <div class="col-sm d-flex flex-column"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="close">Close</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } @else if (step === 'quote') { | ||||||
|  |     <div class="accelerate-cols"> | ||||||
|  |       <ng-container *ngIf="!isMobile"> | ||||||
|  |         <app-accelerate-fee-graph | ||||||
|  |           [tx]="tx" | ||||||
|  |           [estimate]="estimate" | ||||||
|  |           [showEstimate]="hasAccessToBalanceMode" | ||||||
|  |           [maxRateOptions]="maxRateOptions" | ||||||
|  |           [maxRateIndex]="selectFeeRateIndex" | ||||||
|  |           (setUserBid)="setUserBid($event)" | ||||||
|  |         ></app-accelerate-fee-graph> | ||||||
|  |       </ng-container> | ||||||
|  | 
 | ||||||
|  |       <ng-container *ngIf="estimate else loadingEstimate"> | ||||||
|  |         <div> | ||||||
|  |           @if (showDetails) { | ||||||
|  |             <h5 i18n="accelerator.your-transaction">Your transaction</h5> | ||||||
|  |             <div class="row"> | ||||||
|  |               <div class="col"> | ||||||
|  |                 <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> | ||||||
|  |                   <ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container> | ||||||
|  |                 </small> | ||||||
|  |                 <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||||
|  |                   <tbody> | ||||||
|  |                     <tr class="group-first"> | ||||||
|  |                       <td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> | ||||||
|  |                       <td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr class="info"> | ||||||
|  |                       <td class="info" colspan=3> | ||||||
|  |                         <i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr> | ||||||
|  |                       <td class="item" i18n="accelerator.in-band-fees">In-band fees</td> | ||||||
|  |                       <td style="text-align: end;"> | ||||||
|  |                         {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr class="info group-last"> | ||||||
|  |                       <td class="info" colspan=3> | ||||||
|  |                         <i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   </tbody> | ||||||
|  |                 </table> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |             <br> | ||||||
|  |           } | ||||||
|  |           <h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5> | ||||||
|  |           <div class="row"> | ||||||
|  |             <div class="col"> | ||||||
|  |               <ng-container *ngIf="(etaInfo$ | async) as etaInfo; else loadingEstimate"> | ||||||
|  |                 <small class="form-text checkout-text mb-2"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small> | ||||||
|  |                 <small class="form-text checkout-text mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <strong><app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></strong></small> | ||||||
|  |               </ng-container> | ||||||
|  |             </div> | ||||||
|  |             <div class="col pie"> | ||||||
|  |               <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="row"> | ||||||
|  |             <div class="col"> | ||||||
|  |               <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 + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.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 i18n="accelerator.summary-title">Summary</h5> | ||||||
|  |           <div class="row"> | ||||||
|  |             <div class="col"> | ||||||
|  |               <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||||
|  |                 <tbody> | ||||||
|  |                   <!-- ESTIMATED FEE --> | ||||||
|  |                   <ng-container *ngIf="showDetails"> | ||||||
|  |                     @if (hasAccessToBalanceMode) { | ||||||
|  |                       <tr class="group-first"> | ||||||
|  |                         <td class="item" i18n="accelerator.next-block-rate">Next block market rate</td> | ||||||
|  |                         <td class="amt" style="font-size: 16px"> | ||||||
|  |                           {{ 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 i18n="accelerator.estimated-extra-fee-required">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</span> | ||||||
|  |                           <span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> | ||||||
|  |                         </td> | ||||||
|  |                       </tr> | ||||||
|  |                     } | ||||||
|  |                     @else { | ||||||
|  |                       <!-- TARGET FEE --> | ||||||
|  |                       <tr class="group-first"> | ||||||
|  |                         <td class="item" i18n="accelerator.target-rate">Target rate</td> | ||||||
|  |                         <td class="amt" style="font-size: 16px"> | ||||||
|  |                           {{ maxRateOptions[selectFeeRateIndex].rate | 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 i18n="accelerator.extra-fee-required">Extra fee required</small></i> | ||||||
|  |                         </td> | ||||||
|  |                         <td class="amt"> | ||||||
|  |                           {{ maxRateOptions[selectFeeRateIndex].fee | number }} | ||||||
|  |                         </td> | ||||||
|  |                         <td class="units"> | ||||||
|  |                           <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                           <span class="fiat ml-1"><app-fiat [value]="maxRateOptions[selectFeeRateIndex].fee"></app-fiat></span> | ||||||
|  |                         </td> | ||||||
|  |                       </tr> | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     <!-- MEMPOOL BASE FEE --> | ||||||
|  |                     <tr> | ||||||
|  |                       <td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee"> | ||||||
|  |                       <td class="info"> | ||||||
|  |                         <i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i> | ||||||
|  |                       </td> | ||||||
|  |                       <td class="amt"> | ||||||
|  |                         +{{ estimate.mempoolBaseFee | number }} | ||||||
|  |                       </td> | ||||||
|  |                       <td class="units"> | ||||||
|  |                         <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                         <span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                     <tr class="info group-last dashed-bottom" *ngIf="estimate.vsizeFee"> | ||||||
|  |                       <td class="info"> | ||||||
|  |                         <i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i> | ||||||
|  |                       </td> | ||||||
|  |                       <td class="amt"> | ||||||
|  |                         +{{ estimate.vsizeFee | number }} | ||||||
|  |                       </td> | ||||||
|  |                       <td class="units"> | ||||||
|  |                         <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                         <span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   </ng-container> | ||||||
|  | 
 | ||||||
|  |                   <!-- NEXT BLOCK ESTIMATE --> | ||||||
|  |                   <ng-container *ngIf="hasAccessToBalanceMode"> | ||||||
|  |                     <tr class="group-first"> | ||||||
|  |                       <td class="item"> | ||||||
|  |                         <b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB | ||||||
|  |                       </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</span> | ||||||
|  |                         <span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   </ng-container> | ||||||
|  | 
 | ||||||
|  |                   <!-- MAX COST --> | ||||||
|  |                   <ng-container> | ||||||
|  |                     <tr class="group-first group-last"> | ||||||
|  |                       <td class="item"> | ||||||
|  |                         @if (hasAccessToBalanceMode) { | ||||||
|  |                           <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b> | ||||||
|  |                         } @else { | ||||||
|  |                           <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.cost">Acceleration cost</b> | ||||||
|  |                         } | ||||||
|  |                       </td> | ||||||
|  |                       <td class="amt"> | ||||||
|  |                         <span style="background-color: var(--primary)" class="p-1 pl-0"> | ||||||
|  |                           {{ cost | number }} | ||||||
|  |                         </span> | ||||||
|  |                       </td> | ||||||
|  |                       <td class="units"> | ||||||
|  |                         <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                         <span class="fiat ml-1"> | ||||||
|  |                           <app-fiat [value]="cost" [colorClass]="hasAccessToBalanceMode && estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat> | ||||||
|  |                         </span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   </ng-container> | ||||||
|  | 
 | ||||||
|  |                   <!-- USER BALANCE --> | ||||||
|  |                   <ng-container *ngIf="hasAccessToBalanceMode && estimate.userBalance < cost"> | ||||||
|  |                     <tr class="group-first group-last dashed-top"> | ||||||
|  |                       <td class="item" i18n="accelerator.available-balance">Available balance</td> | ||||||
|  |                       <td class="amt"> | ||||||
|  |                         {{ estimate.userBalance | number }} | ||||||
|  |                       </td> | ||||||
|  |                       <td class="units"> | ||||||
|  |                         <span class="symbol" i18n="shared.sats">sats</span> | ||||||
|  |                         <span class="fiat ml-1"> | ||||||
|  |                           <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat> | ||||||
|  |                         </span> | ||||||
|  |                       </td> | ||||||
|  |                     </tr> | ||||||
|  |                   </ng-container> | ||||||
|  | 
 | ||||||
|  |                   <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||||
|  |                     <td class="item"></td> | ||||||
|  |                     <td colspan="2"> | ||||||
|  |                       <div class="d-flex"> | ||||||
|  |                         <ng-container *ngTemplateOutlet="accelerateButton"></ng-container> | ||||||
|  |                       </div> | ||||||
|  |                     </td> | ||||||
|  |                   </tr> | ||||||
|  |                 </tbody> | ||||||
|  |               </table> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </ng-container> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <hr> | ||||||
|  |     <div class="row mt-2 mb-2 text-center"> | ||||||
|  |       <div class="col-sm d-flex flex-column"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <ng-template #loadingEstimate> | ||||||
|  |       <div class="skeleton-loader"></div> | ||||||
|  |       <br> | ||||||
|  |     </ng-template> | ||||||
|  |   } | ||||||
|  |   @else if (step === 'summary') { | ||||||
|  |     <ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingSummary"> | ||||||
|  |       <!-- Show A/B CTAs --> | ||||||
|  |       @if (!noCTA) { | ||||||
|  |         <div class="row mb-1"> | ||||||
|  |           <div class="col-sm"> | ||||||
|  |             <h1 style="font-size: larger;"><ng-content select="[slot='cta-title']"></ng-content><span class="default-slot" i18n="accelerator.accelerate-your-transaction">Accelerate your Bitcoin transaction?</span></h1> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       @if (!advancedEnabled) { | ||||||
|  |         <form> | ||||||
|  |           <div class="row"> | ||||||
|  |             <div class="col-md"> | ||||||
|  |               <div class="form-group form-check mb-2"> | ||||||
|  |                 <input type="radio" [checked]="selectedOption === 'wait'"  class="form-check-input" id="wait" name="accel" (change)="selectedOptionChanged($event)"> | ||||||
|  |                 <label class="form-check-label d-flex flex-column" for="wait"> | ||||||
|  |                   <span class="font-weight-bold" i18n="accelerator.wait">Wait</span> | ||||||
|  |                   @if (eta.blocks < 7) { | ||||||
|  |                     <span class="checkout-text"><ng-container i18n="accelerator.confirmation-expected">Confirmation expected</ng-container> <app-time kind="within" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time></span> | ||||||
|  |                   } @else { | ||||||
|  |                     <span class="checkout-text"> | ||||||
|  |                       <span i18n="accelerator.confirmation-not-expected-soon">Confirmation not expected any time soon</span> | ||||||
|  |                     </span> | ||||||
|  |                   } | ||||||
|  |                 </label> | ||||||
|  |               </div> | ||||||
|  |               <div class="form-group form-check mb-2"> | ||||||
|  |                 <input type="radio" [checked]="selectedOption === 'accel'"  class="form-check-input" id="accel" name="accel" (change)="selectedOptionChanged($event)"> | ||||||
|  |                 <label class="form-check-label d-flex flex-column" for="accel"> | ||||||
|  |                   <ng-container *ngTemplateOutlet="accelerateOption; context: {etaInfo}"></ng-container> | ||||||
|  |                 </label> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="row mt-2 mb-2"> | ||||||
|  |             <div class="col-sm d-flex flex-row justify-content-center"> | ||||||
|  |               <ng-container *ngTemplateOutlet="accelerateButton"></ng-container> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </form> | ||||||
|  |       } @else { | ||||||
|  |         <div> | ||||||
|  |           <div class="row summary-row"> | ||||||
|  |             <div> | ||||||
|  |               <div class="mb-2"> | ||||||
|  |                 <div class="d-flex flex-column" for="accel"> | ||||||
|  |                   <ng-container *ngTemplateOutlet="accelerateOption; context: {etaInfo}"></ng-container> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="pie d-none d-lg-flex"> | ||||||
|  |               <small class="form-text checkout-text mb-2"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small> | ||||||
|  |               <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box> | ||||||
|  |             </div> | ||||||
|  |             <ng-container *ngTemplateOutlet="accelerateButton"></ng-container> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       } | ||||||
|  |     </ng-container> | ||||||
|  |     <ng-template #loadingSummary> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col-md"> | ||||||
|  |           <div class="d-flex flex-row justify-content-center align-items-center"> | ||||||
|  |             <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </ng-template> | ||||||
|  |   } @else if (step === 'checkout') { | ||||||
|  |     <ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingCheckout"> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col-md"> | ||||||
|  |           <div class="d-flex flex-column"> | ||||||
|  |             <span><ng-container *ngTemplateOutlet="accelerateTo; context: {$implicit:(userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize}"></ng-container></span> | ||||||
|  |             <span class="checkout-text"> | ||||||
|  |               @if (!calculating) { | ||||||
|  |                 <ng-container i18n="accelerator.for-an-additional-cost">For an additional</ng-container> <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||||
|  |               } @else { | ||||||
|  |                 <span class="estimating">Calculating cost...</span> | ||||||
|  |               } | ||||||
|  |             </span> | ||||||
|  |             <span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo"> | ||||||
|  |               <ng-container i18n="accelerator.reducing-expected-confirmation-time">Reducing expected confirmation time to <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></ng-container> | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="col-md pie d-none d-md-flex" *ngIf="!forceMobile"> | ||||||
|  |           <small class="form-text checkout-text mb-2" *ngIf="(etaInfo$ | async) as etaInfo"><ng-container *ngTemplateOutlet="prioritizedBy; context: {$implicit:etaInfo.hashratePercentage}"></ng-container></small> | ||||||
|  |           <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="payment-area mt-2 p-2" style="font-size: 14px;"> | ||||||
|  |         <div class="row text-center justify-content-center mx-2"> | ||||||
|  |           <p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p> | ||||||
|  |         </div> | ||||||
|  |         @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { | ||||||
|  |           <div class="row"> | ||||||
|  |             <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |               <p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container> <small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></p> | ||||||
|  |               <div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || quoteError || accelerateError || showSuccess"> | ||||||
|  |                 <ng-container *ngTemplateOutlet="accountPayButton"></ng-container> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         } @else { | ||||||
|  |           <div class="row"> | ||||||
|  |             @if (canPayWithBitcoin) { | ||||||
|  |               <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |                 @if (invoice) { | ||||||
|  |                   <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p> | ||||||
|  |                   <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice> | ||||||
|  |                 } @else if (btcpayInvoiceFailed) { | ||||||
|  |                   <p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p> | ||||||
|  |                   <div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;"> | ||||||
|  |                     <fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon> | ||||||
|  |                   </div> | ||||||
|  |                 } @else { | ||||||
|  |                   <p i18n="accelerator.loading-invoice">Loading invoice...</p> | ||||||
|  |                   <div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;"> | ||||||
|  |                     <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |                   </div> | ||||||
|  |                 } | ||||||
|  |               </div> | ||||||
|  |               @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { | ||||||
|  |                 <div class="col-sm text-center flex-grow-0  d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |                   <p class="text-nowrap">—<span i18n="or">OR</span>—</p> | ||||||
|  |                 </div> | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             @if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { | ||||||
|  |               <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||||
|  |                 <p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p> | ||||||
|  |                 @if (canPayWithCashapp) { | ||||||
|  |                   <img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')"> | ||||||
|  |                 } | ||||||
|  |                 @if (canPayWithApplePay) { | ||||||
|  |                   @if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> } | ||||||
|  |                   <div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')"> | ||||||
|  |                     <img src="/resources/apple-pay.png" height=37> | ||||||
|  |                   </div> | ||||||
|  |                 } | ||||||
|  |                 @if (canPayWithGooglePay) { | ||||||
|  |                   @if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> } | ||||||
|  |                   <div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')"> | ||||||
|  |                     <img src="/resources/google-pay.png" height=37> | ||||||
|  |                   </div> | ||||||
|  |                 } | ||||||
|  |               </div> | ||||||
|  |             } | ||||||
|  |           </div> | ||||||
|  |         } | ||||||
|  |       </div> | ||||||
|  |     </ng-container> | ||||||
|  |     <ng-template #loadingCheckout> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col-md"> | ||||||
|  |           <div class="d-flex flex-row justify-content-center align-items-center"> | ||||||
|  |             <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </ng-template> | ||||||
|  | 
 | ||||||
|  |     <hr> | ||||||
|  |     <div class="row mt-2 mb-2 text-center"> | ||||||
|  |       <div class="col-sm d-flex flex-column"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') { | ||||||
|  |     <!-- Show checkout page --> | ||||||
|  |     <div class="row mb-md-1 text-center" id="confirm-title"> | ||||||
|  |       <div class="col-sm" id="confirm-payment-title"> | ||||||
|  |         <h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot" i18n="accelerator.confirm-your-payment">Confirm your payment</span></h1> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="row text-center"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="form-group w-100" style="font-size: 14px"> | ||||||
|  |           <ng-container i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></ng-container> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     @if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) { | ||||||
|  |       <div class="row text-center mt-1"> | ||||||
|  |         <div class="col-sm"> | ||||||
|  |           <div class="form-group w-100"> | ||||||
|  |             <span><u><strong i18n="accelerator.total-additional-cost">Total additional cost</strong></u><br> | ||||||
|  |               <span style="font-size: 16px" class="d-block mt-2"> | ||||||
|  |                 <ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> | ||||||
|  |                 <strong><app-fiat [value]="cost"></app-fiat></strong> | ||||||
|  |                 <ng-container i18n="accelerator.pay-with">with</ng-container> | ||||||
|  |               </span> | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     <div class="row text-center mt-1"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="form-group w-100"> | ||||||
|  |           @if (step === 'applepay') { | ||||||
|  |             <div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px"  [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div> | ||||||
|  |           } @else if (step === 'cashapp') {           | ||||||
|  |             <div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div> | ||||||
|  |           } @else if (step === 'googlepay') { | ||||||
|  |             <div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div> | ||||||
|  |           } | ||||||
|  |           @if (loadingCashapp || loadingApplePay || loadingGooglePay) { | ||||||
|  |           <div display="d-flex flex-row justify-content-center"> | ||||||
|  |             <span i18n="accelerator.loading-payment-method">Loading payment method...</span> | ||||||
|  |             <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |           </div> | ||||||
|  |           } | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <hr> | ||||||
|  |     <div class="row mt-2 mb-2 text-center"> | ||||||
|  |       <div class="col-sm d-flex flex-column"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('checkout')" i18n="go-back">Go back</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  |   @else if (step === 'processing') { | ||||||
|  |     <div class="row mb-1 text-center"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <h1 style="font-size: larger;"><ng-content select="[slot='processing-title']"></ng-content><span class="default-slot" i18n="accelerator.confirming-your-payment">Confirming your payment</span></h1> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="row text-center mt-1"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="form-group w-100"> | ||||||
|  |           <!-- Processing payment --> | ||||||
|  |           <div id="cash-app-pay" class="d-inline-block" [style]="'opacity: 0; width: 0px; height: 0px; pointer-events: none;'"></div> | ||||||
|  |           <div display="d-flex flex-row justify-content-center"> | ||||||
|  |             <span i18n="accelerator.payment-processing">We are processing your payment...</span> | ||||||
|  |             <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  |   @else if (step === 'paid') { | ||||||
|  |     <div class="row mb-1 text-center"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <h1 style="font-size: larger;"><ng-content select="[slot='accelerating-title']"></ng-content><span class="default-slot" i18n="accelerator.accelerating-your-transaction">Accelerating your transaction</span></h1> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="row text-center mt-1"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="d-flex flex-row flex-column justify-content-center align-items-center"> | ||||||
|  |           <span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span> | ||||||
|  |           @if (timeSincePaid > 30000) { | ||||||
|  |             <span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span> | ||||||
|  |           } | ||||||
|  |           <div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } @else if (step === 'success') { | ||||||
|  |     <div class="row mb-1 text-center"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content><span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span></h1> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div class="row text-center mt-1"> | ||||||
|  |       <div class="col-sm"> | ||||||
|  |         <div class="d-flex flex-row justify-content-center align-items-center"> | ||||||
|  |           <span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <hr> | ||||||
|  |     <div class="row mt-2 mb-2 text-center"> | ||||||
|  |       <div class="col-sm d-flex flex-column"> | ||||||
|  |         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="closeModal()" i18n="close">Close</button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <ng-template #accelerateOption let-etaInfo="etaInfo"> | ||||||
|  |   <span><ng-container *ngTemplateOutlet="accelerateTo; context: {$implicit:(userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize}"></ng-container> <ng-container *ngTemplateOutlet="customizeButton"></ng-container></span> | ||||||
|  |   <span class="checkout-text"><ng-container i18n="accelerator.confirmation-expected">Confirmation expected</ng-container> <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br> | ||||||
|  |     @if (!calculating) { | ||||||
|  |       <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||||
|  |     } @else { | ||||||
|  |       <span class="estimating" i18n="accelerator.calculating-cost">Calculating cost...</span> | ||||||
|  |     } | ||||||
|  |   </span> | ||||||
|  | </ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template #customizeButton> | ||||||
|  |   <button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-2" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button> | ||||||
|  | </ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template id="accelerate-to" #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template #accelerateButton> | ||||||
|  |   <div class="position-relative"> | ||||||
|  |     <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')"> | ||||||
|  |       <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||||
|  |       <span i18n="transaction.accelerate|Accelerate button label">Accelerate</span> | ||||||
|  |     </button> | ||||||
|  |     @if (quoteError || cantPayReason) { | ||||||
|  |       <div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div> | ||||||
|  |     } | ||||||
|  |   </div> | ||||||
|  | </ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template #accountPayButton> | ||||||
|  |   @if (hasAccessToBalanceMode) { | ||||||
|  |     <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || calculating" style="width: 200px" (click)="accelerateWithMempoolAccount()"> | ||||||
|  |       <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||||
|  |       <span i18n="transaction.pay|Pay button label">Pay</span> | ||||||
|  |     </button> | ||||||
|  |   } @else { | ||||||
|  |     <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px"> | ||||||
|  |       <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||||
|  |       <span>Coming soon</span> | ||||||
|  |     </button> | ||||||
|  |   } | ||||||
|  | </ng-template> | ||||||
|  | 
 | ||||||
|  | <ng-template #prioritizedBy let-i i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to <strong>{{ i | number : '1.1-1' }}%</strong> of miners.</ng-template> | ||||||
| @ -0,0 +1,219 @@ | |||||||
|  | .close-button { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0.5em; | ||||||
|  |   right: 0.5em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .estimating { | ||||||
|  |   color: var(--green) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .paymentMethod { | ||||||
|  |   padding: 10px; | ||||||
|  |   background-color: var(--secondary); | ||||||
|  |   border-radius: 10px; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .default-slot:not(:only-child) { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .pie { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   max-width: 330px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fee-card { | ||||||
|  |   padding: 15px; | ||||||
|  |   background-color: var(--bg); | ||||||
|  | 
 | ||||||
|  |   .feerate { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  | 
 | ||||||
|  |     .rate { | ||||||
|  |       font-size: 0.9em; | ||||||
|  |       .symbol { | ||||||
|  |         color: white; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-border { | ||||||
|  |   border: solid 1px black; | ||||||
|  |   background-color: #0c4a87; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .feerate.active { | ||||||
|  |   background-color: var(--primary) !important; | ||||||
|  |   opacity: 1; | ||||||
|  |   border: 1px solid #007fff !important; | ||||||
|  | } | ||||||
|  | .feerate:focus { | ||||||
|  |   box-shadow: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .grayOut { | ||||||
|  |   opacity: 0.5; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .disabled { | ||||||
|  |   opacity: 0.5; | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .table-toggle { | ||||||
|  |   width: 100%; | ||||||
|  |   margin-top: 0.5em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tab { | ||||||
|  |   &:first-child { | ||||||
|  |     margin-right: 1px; | ||||||
|  |   } | ||||||
|  |   border: solid 1px black; | ||||||
|  |   border-bottom: none; | ||||||
|  |   background-color: #323655; | ||||||
|  |   border-top-left-radius: 10px !important; | ||||||
|  |   border-top-right-radius: 10px !important; | ||||||
|  | } | ||||||
|  | .tab.active { | ||||||
|  |   background-color: #5d659d !important; | ||||||
|  |   opacity: 1; | ||||||
|  | } | ||||||
|  | .tab:focus { | ||||||
|  |   box-shadow: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .table-accelerator { | ||||||
|  |   tr { | ||||||
|  |     td { | ||||||
|  |       padding-top: 0; | ||||||
|  |       padding-bottom: 0; | ||||||
|  |       vertical-align: baseline; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &.group-first { | ||||||
|  |       td { | ||||||
|  |         padding-top: 0.75rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &.group-last, &:last-child { | ||||||
|  |       td { | ||||||
|  |         padding-bottom: 0.75rem; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &.dashed-top { | ||||||
|  |       border-top: 1px dashed grey; | ||||||
|  |     } | ||||||
|  |     &.dashed-bottom { | ||||||
|  |       border-bottom: 1px dashed grey | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   td { | ||||||
|  |     &:first-child { | ||||||
|  |       width: 100vw; | ||||||
|  |     } | ||||||
|  |     &.info { | ||||||
|  |       color: #6c757d; | ||||||
|  |       white-space: initial; | ||||||
|  |     } | ||||||
|  |     &.amt { | ||||||
|  |       text-align: right; | ||||||
|  |       padding-right: 0.2em; | ||||||
|  |     } | ||||||
|  |     &.units { | ||||||
|  |       padding-left: 0.2em; | ||||||
|  |       white-space: nowrap; | ||||||
|  |       display: flex; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       align-items: center; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .accelerate-cols { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   align-items: stretch; | ||||||
|  |   margin-top: 1em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .payment-area { | ||||||
|  |   background: var(--bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .col.pie { | ||||||
|  |   flex-grow: 0; | ||||||
|  |   padding: 0 1em; | ||||||
|  |   position: relative; | ||||||
|  |   top: -15px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item { | ||||||
|  |   white-space: initial; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .table-background { | ||||||
|  |   background-color: var(--bg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .checkout-text { | ||||||
|  |   color: rgb(186, 186, 186); | ||||||
|  |   font-size: 14px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-accelerate { | ||||||
|  |   background-color: var(--tertiary); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-small-height { | ||||||
|  | 	line-height: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .summary-row { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|  |   padding: 0 2em; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  | 
 | ||||||
|  |   @media (max-width: 640px) { | ||||||
|  |     flex-direction: column; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-error { | ||||||
|  |   position: absolute; | ||||||
|  |   right: 0; | ||||||
|  |   font-size: 12px; | ||||||
|  |   color: var(--red); | ||||||
|  |   text-align: center; | ||||||
|  |   width: 200px; | ||||||
|  |   white-space: normal; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .btn-error-wrapper { | ||||||
|  |   height: 26px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .apple-pay-button { | ||||||
|  |     display: inline-block; | ||||||
|  |     -webkit-appearance: -apple-pay-button; | ||||||
|  |     -apple-pay-button-type: plain; /* Use any supported button type. */ | ||||||
|  | } | ||||||
|  | .apple-pay-button-black { | ||||||
|  |     -apple-pay-button-style: black; | ||||||
|  | } | ||||||
|  | .apple-pay-button-white { | ||||||
|  |     -apple-pay-button-style: white; | ||||||
|  | } | ||||||
|  | .apple-pay-button-white-with-line { | ||||||
|  |     -apple-pay-button-style: white-outline; | ||||||
|  | } | ||||||
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