Merge branch 'master' into nymkappa/health-check

This commit is contained in:
nymkappa 2024-09-06 10:10:36 +02:00
commit cef6127e69
No known key found for this signature in database
GPG Key ID: 92358FC85D9645DE
590 changed files with 114171 additions and 48272 deletions

View File

@ -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
View File

@ -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>.

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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__"
} }
} }

View File

@ -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);
});
});
}); });
}); });

View 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
}
}
]

View File

@ -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
}
} }
] ]

View File

@ -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: '',
}); });
}); });

View File

@ -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);

View File

@ -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 });

View File

@ -1,49 +1,44 @@
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 });
@ -57,7 +52,7 @@ 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 });
@ -70,6 +65,20 @@ class AccelerationRoutes {
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();

View File

@ -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);
@ -176,94 +150,14 @@ class AccelerationCosts {
*/ */
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
@ -271,7 +165,7 @@ class AccelerationCosts {
* @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
);
}

View File

@ -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,

View File

@ -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[];

View File

@ -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,
}

View File

@ -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'
}; };

View File

@ -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();

View File

@ -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;

View File

@ -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();
} }

View File

@ -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);

View File

@ -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;
} }
} }

View File

@ -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 {
@ -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

View File

@ -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);
} }
} }
@ -200,87 +270,3 @@ function calculateCpfpCluster(txid: string, graph: Map<string, GraphTx>): Map<st
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;
}

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 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();

View File

@ -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);

View File

@ -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) {

View File

@ -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',

View File

@ -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();

View File

@ -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(),

View 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
);
}

View File

@ -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();

View File

@ -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 {

View File

@ -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[] = [];
@ -43,22 +46,47 @@ class PoolsParser {
// 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;
} }
} }

View File

@ -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();

View File

@ -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> {

View File

@ -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();

View File

@ -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();

View File

@ -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})`);

View File

@ -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': '',
}, },
}; };

View File

@ -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';

View File

@ -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);
} }

View File

@ -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) {

View File

@ -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
} }

View File

@ -35,7 +35,7 @@ class AuditReplication {
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 || [],

View 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();

View File

@ -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();

View File

@ -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();

View File

@ -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,
@ -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

View File

@ -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
* *

View File

@ -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();

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;
} }
} }

View File

@ -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;
} }

View File

@ -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);

View 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
View 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

View 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

View 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
View 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

View 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
View 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

View File

@ -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: ""

View File

@ -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

View File

@ -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__"
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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__

View File

@ -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
View File

@ -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

View File

@ -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:

View File

@ -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,

View 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"
}
}
]
}
}

View File

@ -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`);

View File

@ -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();
}); });

View File

@ -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)', () => {

View File

@ -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(() => {

View File

@ -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(() => {

View File

@ -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
}, },

View File

@ -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();

View File

@ -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>
} }
} }

View File

@ -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;

View File

@ -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"
} }

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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,

View File

@ -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`,

View File

@ -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',

View File

@ -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'],
} }
}; };

View File

@ -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 },
]; ];

View File

@ -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('');

View File

@ -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;

View File

@ -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&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, 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&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, 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 &lt;https://mempool.space/trademark-policy&gt;. While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@ -10,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;
}
}

View File

@ -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]="'&lrm;' + (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>&nbsp;<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>&nbsp;<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>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<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>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<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">&mdash;<span i18n="or">OR</span>&mdash;</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>&nbsp;<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>&nbsp;<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>&nbsp;<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>

View File

@ -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