Compare commits

..

1 Commits

Author SHA1 Message Date
wiz
ff8495f7cf Add Maxmind GeoIP Lite download to Docker build 2023-03-19 17:33:51 +09:00
229 changed files with 3776 additions and 9677 deletions

View File

@@ -1,13 +1,13 @@
# The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs) # The Mempool Open Source Project™ [![mempool](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ry4br7/master&style=flat-square)](https://dashboard.cypress.io/projects/ry4br7/runs)
https://user-images.githubusercontent.com/93150691/226236121-375ea64f-b4a1-4cc0-8fad-a6fb33226840.mp4 https://user-images.githubusercontent.com/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4
<br>
Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/). Mempool is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/).
It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem. It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.4.0-dashboard.png)
# Installation Methods # Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server. Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.

View File

@@ -27,15 +27,13 @@
"AUDIT": false, "AUDIT": false,
"ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool"
"TIMEOUT": 60000
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -43,16 +41,13 @@
"TLS_ENABLED": true "TLS_ENABLED": true
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000", "REST_API_URL": "http://127.0.0.1:3000"
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"RETRY_UNIX_SOCKET_AFTER": 30000
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool"
"TIMEOUT": 60000
}, },
"DATABASE": { "DATABASE": {
"ENABLED": true, "ENABLED": true,
@@ -61,8 +56,7 @@
"SOCKET": "/var/run/mysql/mysql.sock", "SOCKET": "/var/run/mysql/mysql.sock",
"DATABASE": "mempool", "DATABASE": "mempool",
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool"
"TIMEOUT": 180000
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": true, "ENABLED": true,
@@ -97,8 +91,7 @@
"LND": { "LND": {
"TLS_CERT_PATH": "tls.cert", "TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon", "MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080", "REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "lightning-rpc" "SOCKET": "lightning-rpc"

6611
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.6.0-dev", "version": "2.5.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",
@@ -34,35 +34,35 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.20.12",
"@mempool/electrum-client": "1.1.9", "@mempool/electrum-client": "^1.1.7",
"@types/node": "^18.15.3", "@types/node": "^16.18.11",
"axios": "~0.27.2", "axios": "~0.27.2",
"bitcoinjs-lib": "~6.1.0", "bitcoinjs-lib": "~6.1.0",
"crypto-js": "~4.1.1", "crypto-js": "~4.1.1",
"express": "~4.18.2", "express": "~4.18.2",
"maxmind": "~4.3.8", "maxmind": "~4.3.8",
"mysql2": "~3.2.0", "mysql2": "~2.3.3",
"node-worker-threads-pool": "~1.5.1", "node-worker-threads-pool": "~1.5.1",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"ws": "~8.13.0" "ws": "~8.11.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.20.7",
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
"@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.15", "@types/express": "^4.17.15",
"@types/jest": "^29.5.0", "@types/jest": "^29.2.5",
"@types/ws": "~8.5.4", "@types/ws": "~8.5.4",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.36.0", "eslint": "^8.31.0",
"eslint-config-prettier": "^8.7.0", "eslint-config-prettier": "^8.5.0",
"jest": "^29.5.0", "jest": "^29.3.1",
"prettier": "^2.8.4", "prettier": "^2.8.2",
"ts-jest": "^29.0.5", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
} }
} }

View File

@@ -28,15 +28,13 @@
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", "ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", "ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__", "CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__", "MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__"
"DISK_CACHE_BLOCK_INTERVAL": "__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": 15, "PORT": 15,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__"
"TIMEOUT": "__CORE_RPC_TIMEOUT__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -44,16 +42,13 @@
"TLS_ENABLED": true "TLS_ENABLED": true
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__"
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": "__ESPLORA_RETRY_UNIX_SOCKET_AFTER__"
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": 17, "PORT": 17,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
"TIMEOUT": "__SECOND_CORE_RPC_TIMEOUT__"
}, },
"DATABASE": { "DATABASE": {
"ENABLED": false, "ENABLED": false,
@@ -62,8 +57,7 @@
"PORT": 18, "PORT": 18,
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__", "PASSWORD": "__DATABASE_PASSWORD__"
"TIMEOUT": "__DATABASE_TIMEOUT__"
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": false, "ENABLED": false,
@@ -113,8 +107,7 @@
"LND": { "LND": {
"TLS_CERT_PATH": "", "TLS_CERT_PATH": "",
"MACAROON_PATH": "", "MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080", "REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"

View File

@@ -42,27 +42,24 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_MEMPOOL: false, ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, MAX_BLOCKS_BULK_QUERY: 0,
DISK_CACHE_BLOCK_INTERVAL: 6,
}); });
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true }); expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 }); expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000' });
expect(config.CORE_RPC).toStrictEqual({ expect(config.CORE_RPC).toStrictEqual({
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool'
TIMEOUT: 60000
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 8332, PORT: 8332,
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool'
TIMEOUT: 60000
}); });
expect(config.DATABASE).toStrictEqual({ expect(config.DATABASE).toStrictEqual({
@@ -72,8 +69,7 @@ describe('Mempool Backend Config', () => {
PORT: 3306, PORT: 3306,
DATABASE: 'mempool', DATABASE: 'mempool',
USERNAME: 'mempool', USERNAME: 'mempool',
PASSWORD: 'mempool', PASSWORD: 'mempool'
TIMEOUT: 180000,
}); });
expect(config.SYSLOG).toStrictEqual({ expect(config.SYSLOG).toStrictEqual({
@@ -112,10 +108,10 @@ describe('Mempool Backend Config', () => {
}); });
expect(config.MAXMIND).toStrictEqual({ expect(config.MAXMIND).toStrictEqual({
ENABLED: false, ENABLED: true,
GEOLITE2_CITY: '/usr/local/share/GeoIP/GeoLite2-City.mmdb', GEOLITE2_CITY: './backend/GeoIP/GeoLite2-City.mmdb',
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb', GEOLITE2_ASN: './backend/GeoIP/GeoLite2-ASN.mmdb',
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb' GEOIP2_ISP: ''
}); });
}); });
}); });

View File

@@ -1,5 +1,4 @@
import config from '../config'; import config from '../config';
import logger from '../logger';
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
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
@@ -40,19 +39,17 @@ class Audit {
} else { } else {
isCensored[txid] = true; isCensored[txid] = true;
} }
displacedWeight += mempool[txid]?.weight || 0; displacedWeight += mempool[txid].weight;
} else { } else {
matchedWeight += mempool[txid]?.weight || 0; matchedWeight += mempool[txid].weight;
} }
projectedWeight += mempool[txid]?.weight || 0; projectedWeight += mempool[txid].weight;
inTemplate[txid] = true; inTemplate[txid] = true;
} }
if (transactions[0]) { displacedWeight += (4000 - transactions[0].weight);
displacedWeight += (4000 - transactions[0].weight); projectedWeight += transactions[0].weight;
projectedWeight += transactions[0].weight; matchedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
}
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs // we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
// these displaced transactions should occupy the first N weight units of the next projected block // these displaced transactions should occupy the first N weight units of the next projected block
@@ -62,24 +59,19 @@ class Audit {
let failures = 0; let failures = 0;
while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) { while (projectedBlocks[1] && index < projectedBlocks[1].transactionIds.length && failures < 500) {
const txid = projectedBlocks[1].transactionIds[index]; const txid = projectedBlocks[1].transactionIds[index];
const tx = mempool[txid]; const fits = (mempool[txid].weight - displacedWeightRemaining) < 4000;
if (tx) { const feeMatches = mempool[txid].effectiveFeePerVsize >= lastFeeRate;
const fits = (tx.weight - displacedWeightRemaining) < 4000; if (fits || feeMatches) {
const feeMatches = tx.effectiveFeePerVsize >= lastFeeRate; isDisplaced[txid] = true;
if (fits || feeMatches) { if (fits) {
isDisplaced[txid] = true; lastFeeRate = Math.min(lastFeeRate, mempool[txid].effectiveFeePerVsize);
if (fits) {
lastFeeRate = Math.min(lastFeeRate, tx.effectiveFeePerVsize);
}
if (tx.firstSeen == null || (now - (tx?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= tx.weight;
}
failures = 0;
} else {
failures++;
} }
if (mempool[txid].firstSeen == null || (now - (mempool[txid]?.firstSeen || 0)) > PROPAGATION_MARGIN) {
displacedWeightRemaining -= mempool[txid].weight;
}
failures = 0;
} else { } else {
logger.warn('projected transaction missing from mempool cache'); failures++;
} }
index++; index++;
} }
@@ -93,7 +85,17 @@ class Audit {
} else { } else {
if (!isDisplaced[tx.txid]) { if (!isDisplaced[tx.txid]) {
added.push(tx.txid); added.push(tx.txid);
} else {
} }
let blockIndex = -1;
let index = -1;
projectedBlocks.forEach((block, bi) => {
const i = block.transactionIds.indexOf(tx.txid);
if (i >= 0) {
blockIndex = bi;
index = i;
}
});
overflowWeight += tx.weight; overflowWeight += tx.weight;
} }
totalWeight += tx.weight; totalWeight += tx.weight;
@@ -106,25 +108,20 @@ class Audit {
index = projectedBlocks[0].transactionIds.length - 1; index = projectedBlocks[0].transactionIds.length - 1;
while (index >= 0) { while (index >= 0) {
const txid = projectedBlocks[0].transactionIds[index]; const txid = projectedBlocks[0].transactionIds[index];
const tx = mempool[txid]; if (overflowWeightRemaining > 0) {
if (tx) { if (isCensored[txid]) {
if (overflowWeightRemaining > 0) { delete isCensored[txid];
if (isCensored[txid]) { }
delete isCensored[txid]; if (mempool[txid].effectiveFeePerVsize > maxOverflowRate) {
} maxOverflowRate = mempool[txid].effectiveFeePerVsize;
if (tx.effectiveFeePerVsize > maxOverflowRate) { rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005;
maxOverflowRate = tx.effectiveFeePerVsize; }
rateThreshold = (Math.ceil(maxOverflowRate * 100) / 100) + 0.005; } else if (mempool[txid].effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding
} if (isCensored[txid]) {
} else if (tx.effectiveFeePerVsize <= rateThreshold) { // tolerance of 0.01 sat/vb + rounding delete isCensored[txid];
if (isCensored[txid]) {
delete isCensored[txid];
}
} }
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
} else {
logger.warn('projected transaction missing from mempool cache');
} }
overflowWeightRemaining -= (mempool[txid]?.weight || 0);
index--; index--;
} }

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.CORE_RPC.PORT, port: config.CORE_RPC.PORT,
user: config.CORE_RPC.USERNAME, user: config.CORE_RPC.USERNAME,
pass: config.CORE_RPC.PASSWORD, pass: config.CORE_RPC.PASSWORD,
timeout: config.CORE_RPC.TIMEOUT, timeout: 60000,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -7,7 +7,7 @@ const nodeRpcCredentials: BitcoinRpcCredentials = {
port: config.SECOND_CORE_RPC.PORT, port: config.SECOND_CORE_RPC.PORT,
user: config.SECOND_CORE_RPC.USERNAME, user: config.SECOND_CORE_RPC.USERNAME,
pass: config.SECOND_CORE_RPC.PASSWORD, pass: config.SECOND_CORE_RPC.PASSWORD,
timeout: config.SECOND_CORE_RPC.TIMEOUT, timeout: 60000,
}; };
export default new bitcoin.Client(nodeRpcCredentials); export default new bitcoin.Client(nodeRpcCredentials);

View File

@@ -32,10 +32,8 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData) .get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm) .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => { .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try { try {
@@ -96,7 +94,6 @@ 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 + '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))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
@@ -113,6 +110,7 @@ class BitcoinRoutes {
.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)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
@@ -591,14 +589,10 @@ class BitcoinRoutes {
} }
} }
private getBlockTipHeight(req: Request, res: Response) { private async getBlockTipHeight(req: Request, res: Response) {
try { try {
const result = blocks.getCurrentBlockHeight(); const result = await bitcoinApi.$getBlockHeightTip();
if (!result) { res.json(result);
return res.status(503).send(`Service Temporarily Unavailable`);
}
res.setHeader('content-type', 'text/plain');
res.send(result.toString());
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@@ -644,30 +638,8 @@ class BitcoinRoutes {
private async getRbfHistory(req: Request, res: Response) { private async getRbfHistory(req: Request, res: Response) {
try { try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null; const result = rbfCache.getReplaces(req.params.txId);
const replaces = rbfCache.getReplaces(req.params.txId) || null; res.json(result || []);
res.json({
replacements,
replaces
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getRbfReplacements(req: Request, res: Response) {
try {
const result = rbfCache.getRbfTrees(false);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getFullRbfReplacements(req: Request, res: Response) {
try {
const result = rbfCache.getRbfTrees(true);
res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }

View File

@@ -16,7 +16,7 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
super(bitcoinClient); super(bitcoinClient);
const electrumConfig = { client: 'mempool-v2', version: '1.4' }; const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 1000, maxRetry: Number.MAX_SAFE_INTEGER, callback: null }; const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
const electrumCallbacks = { const electrumCallbacks = {
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); }, onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },

View File

@@ -3,102 +3,65 @@ import axios, { AxiosRequestConfig } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
const axiosConnection = axios.create({ const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true, }) httpAgent: new http.Agent({ keepAlive: true })
}); });
class ElectrsApi implements AbstractBitcoinApi { class ElectrsApi implements AbstractBitcoinApi {
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? { axiosConfig: AxiosRequestConfig = {
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
timeout: 10000,
} : {
timeout: 10000,
};
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
timeout: 10000, timeout: 10000,
}; };
unixSocketRetryTimeout; constructor() { }
activeAxiosConfig;
constructor() {
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
}
fallbackToTcpSocket() {
if (!this.unixSocketRetryTimeout) {
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
// Retry the unix socket after a few seconds
this.unixSocketRetryTimeout = setTimeout(() => {
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
this.unixSocketRetryTimeout = undefined;
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
}
// Use the TCP socket (reach a different esplora instance through nginx)
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
}
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
.then((response) => response.data)
.catch((e) => {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
// Retry immediately
return axiosConnection.get<T>(url, this.activeAxiosConfig)
.then((response) => response.data)
.catch((e) => {
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
throw e;
});
} else {
throw e;
}
});
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids'); return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
.then((response) => response.data);
} }
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> { $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId); return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
.then((response) => response.data);
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex'); return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
.then((response) => response.data);
} }
$getBlockHeightTip(): Promise<number> { $getBlockHeightTip(): Promise<number> {
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height'); return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
.then((response) => response.data);
} }
$getBlockHashTip(): Promise<string> { $getBlockHashTip(): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash'); return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
.then((response) => response.data);
} }
$getTxIdsForBlock(hash: string): Promise<string[]> { $getTxIdsForBlock(hash: string): Promise<string[]> {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data);
} }
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height); return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
.then((response) => response.data);
} }
$getBlockHeader(hash: string): Promise<string> { $getBlockHeader(hash: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header'); return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
.then((response) => response.data);
} }
$getBlock(hash: string): Promise<IEsploraApi.Block> { $getBlock(hash: string): Promise<IEsploraApi.Block> {
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash); return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
.then((response) => response.data);
} }
$getRawBlock(hash: string): Promise<Buffer> { $getRawBlock(hash: string): Promise<Buffer> {
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer') return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
.then((response) => { return Buffer.from(response.data); }); .then((response) => { return Buffer.from(response.data); });
} }
@@ -119,11 +82,13 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout); return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
.then((response) => response.data);
} }
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> { $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends'); return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
.then((response) => response.data);
} }
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> { async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {

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, TransactionStripped, TransactionMinerInfo, CpfpSummary } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionStripped, TransactionMinerInfo } 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';
@@ -36,8 +36,6 @@ class Blocks {
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = []; private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = []; private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
private mainLoopTimeout: number = 120000;
constructor() { } constructor() { }
public getBlocks(): BlockExtended[] { public getBlocks(): BlockExtended[] {
@@ -202,15 +200,8 @@ class Blocks {
extras.segwitTotalWeight = 0; extras.segwitTotalWeight = 0;
} else { } else {
const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id); const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id);
let feeStats = { extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
medianFee: stats.feerate_percentiles[2], // 50th percentiles extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat();
feeRange: [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(),
};
if (transactions?.length > 1) {
feeStats = Common.calcEffectiveFeeStatistics(transactions);
}
extras.medianFee = feeStats.medianFee;
extras.feeRange = feeStats.feeRange;
extras.totalFees = stats.totalfee; extras.totalFees = stats.totalfee;
extras.avgFee = stats.avgfee; extras.avgFee = stats.avgfee;
extras.avgFeeRate = stats.avgfeerate; extras.avgFeeRate = stats.avgfeerate;
@@ -412,13 +403,12 @@ class Blocks {
try { try {
// Get all indexed block hash // Get all indexed block hash
const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks(); const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks();
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
if (!unindexedBlockHeights?.length) { if (!unindexedBlockHeights?.length) {
return; return;
} }
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
// Logging // Logging
let count = 0; let count = 0;
let countThisRun = 0; let countThisRun = 0;
@@ -529,16 +519,9 @@ class Blocks {
return await BlocksRepository.$validateChain(); return await BlocksRepository.$validateChain();
} }
public async $updateBlocks(): Promise<number> { public async $updateBlocks() {
// warn if this run stalls the main loop for more than 2 minutes
const timer = this.startTimer();
diskCache.lock();
let fastForwarded = false; let fastForwarded = false;
let handledBlocks = 0;
const blockHeightTip = await bitcoinApi.$getBlockHeightTip(); const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
this.updateTimerProgress(timer, 'got block height tip');
if (this.blocks.length === 0) { if (this.blocks.length === 0) {
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1); this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
@@ -556,21 +539,16 @@ class Blocks {
if (!this.lastDifficultyAdjustmentTime) { if (!this.lastDifficultyAdjustmentTime) {
const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const blockchainInfo = await bitcoinClient.getBlockchainInfo();
this.updateTimerProgress(timer, 'got blockchain info for initial difficulty adjustment');
if (blockchainInfo.blocks === blockchainInfo.headers) { if (blockchainInfo.blocks === blockchainInfo.headers) {
const heightDiff = blockHeightTip % 2016; const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff); const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash); const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
this.lastDifficultyAdjustmentTime = block.timestamp; this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty; this.currentDifficulty = block.difficulty;
if (blockHeightTip >= 2016) { if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016); const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash); const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100; this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`); logger.debug(`Initial difficulty adjustment data set.`);
} }
@@ -580,71 +558,57 @@ class Blocks {
} }
while (this.currentBlockHeight < blockHeightTip) { while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight === 0) { if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) {
this.currentBlockHeight = blockHeightTip; this.currentBlockHeight = blockHeightTip;
} 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}`);
await chainTips.updateOrphanedBlocks(); await chainTips.updateOrphanedBlocks();
} }
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight); const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2); const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
const block = BitcoinApi.convertBlock(verboseBlock); const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash); const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false); const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions); const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock); const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
// start async callbacks // start async callbacks
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, transactions));
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
if (!fastForwarded) { if (!fastForwarded) {
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1); const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
this.updateTimerProgress(timer, `got block by height for ${this.currentBlockHeight}`);
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) { if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining); logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`);
// We assume there won't be a reorg with more than 10 block depth // We assume there won't be a reorg with more than 10 block depth
this.updateTimerProgress(timer, `rolling back diverged chain from ${this.currentBlockHeight}`);
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10); await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
await HashratesRepository.$deleteLastEntries(); await HashratesRepository.$deleteLastEntries();
await BlocksSummariesRepository.$deleteBlocksFrom(lastBlock.height - 10);
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10); await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
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.updateTimerProgress(timer, `reindexed block`);
await this.$getStrippedBlockTransactions(newBlock.id, true, true); await this.$getStrippedBlockTransactions(newBlock.id, true, true);
this.updateTimerProgress(timer, `reindexed block summary`);
if (config.MEMPOOL.CPFP_INDEXING) { if (config.MEMPOOL.CPFP_INDEXING) {
await this.$indexCPFP(newBlock.id, lastBlock.height - i); await this.$indexCPFP(newBlock.id, lastBlock.height - i);
this.updateTimerProgress(timer, `reindexed block cpfp`);
} }
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment(); await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
this.updateTimerProgress(timer, `reindexed difficulty adjustments`); logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
indexer.reindex(); indexer.reindex();
} }
await blocksRepository.$saveBlockInDatabase(blockExtended); await blocksRepository.$saveBlockInDatabase(blockExtended);
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
if (priceUpdater.historyInserted === true && lastestPriceId !== null) { if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{ await blocksRepository.$saveBlockPrices([{
height: blockExtended.height, height: blockExtended.height,
priceId: lastestPriceId, priceId: lastestPriceId,
}]); }]);
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else { } else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
setTimeout(() => { setTimeout(() => {
indexer.runSingleTask('blocksPrices'); indexer.runSingleTask('blocksPrices');
}, 10000); }, 10000);
@@ -653,11 +617,9 @@ class Blocks {
// Save blocks summary for visualization if it's enabled // Save blocks summary for visualization if it's enabled
if (Common.blocksSummariesIndexingEnabled() === true) { if (Common.blocksSummariesIndexingEnabled() === true) {
await this.$getStrippedBlockTransactions(blockExtended.id, true); await this.$getStrippedBlockTransactions(blockExtended.id, true);
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
} }
if (config.MEMPOOL.CPFP_INDEXING) { if (config.MEMPOOL.CPFP_INDEXING) {
this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary); this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
this.updateTimerProgress(timer, `saved cpfp for ${this.currentBlockHeight}`);
} }
} }
} }
@@ -670,7 +632,6 @@ class Blocks {
difficulty: block.difficulty, difficulty: block.difficulty,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
}); });
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
} }
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100; this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
@@ -690,44 +651,12 @@ class Blocks {
if (this.newBlockCallbacks.length) { if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions)); this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
} }
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) { if (!memPool.hasPriority() && (block.height % 6 === 0)) {
diskCache.$saveCacheToDisk(); diskCache.$saveCacheToDisk();
} }
// wait for pending async callbacks to finish // wait for pending async callbacks to finish
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
await Promise.all(callbackPromises); await Promise.all(callbackPromises);
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
handledBlocks++;
}
diskCache.unlock();
this.clearTimer(timer);
return handledBlocks;
}
private startTimer() {
const state: any = {
start: Date.now(),
progress: 'begin $updateBlocks',
timer: null,
};
state.timer = setTimeout(() => {
logger.err(`$updateBlocks stalled at "${state.progress}"`);
}, this.mainLoopTimeout);
return state;
}
private updateTimerProgress(state, msg) {
state.progress = msg;
}
private clearTimer(state) {
if (state.timer) {
clearTimeout(state.timer);
} }
} }
@@ -799,7 +728,7 @@ class Blocks {
// Index the response if needed // Index the response if needed
if (Common.blocksSummariesIndexingEnabled() === true) { if (Common.blocksSummariesIndexingEnabled() === true) {
await BlocksSummariesRepository.$saveTransactions(block.height, block.hash, summary.transactions); await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary});
} }
return summary.transactions; return summary.transactions;
@@ -915,12 +844,11 @@ class Blocks {
if (cleanBlock.fee_amt_percentiles === null) { if (cleanBlock.fee_amt_percentiles === null) {
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2); const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block); const summary = this.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(cleanBlock.height, cleanBlock.hash, summary.transactions); await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash); cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
} }
if (cleanBlock.fee_amt_percentiles !== null) { if (cleanBlock.fee_amt_percentiles !== null) {
cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3]; cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3];
await blocksRepository.$updateFeeAmounts(cleanBlock.hash, cleanBlock.fee_amt_percentiles, cleanBlock.median_fee_amt);
} }
} }
@@ -985,20 +913,42 @@ class Blocks {
public async $indexCPFP(hash: string, height: number): Promise<void> { public async $indexCPFP(hash: string, height: number): Promise<void> {
const block = await bitcoinClient.getBlock(hash, 2); const block = await bitcoinClient.getBlock(hash, 2);
const transactions = block.tx.map(tx => { const transactions = block.tx.map(tx => {
tx.vsize = tx.weight / 4;
tx.fee *= 100_000_000; tx.fee *= 100_000_000;
return tx; return tx;
}); });
const summary = Common.calculateCpfp(height, transactions); const clusters: any[] = [];
await this.$saveCpfp(hash, height, summary); let cluster: TransactionStripped[] = [];
let ancestors: { [txid: string]: boolean } = {};
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions); for (let i = transactions.length - 1; i >= 0; i--) {
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats); const tx = transactions[i];
} if (!ancestors[tx.txid]) {
let totalFee = 0;
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> { let totalVSize = 0;
const result = await cpfpRepository.$batchSaveClusters(cpfpSummary.clusters); cluster.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += tx.vsize;
});
const effectiveFeePerVsize = totalFee / totalVSize;
if (cluster.length > 1) {
clusters.push({
root: cluster[0].txid,
height,
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
});
}
cluster = [];
ancestors = {};
}
cluster.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
const result = await cpfpRepository.$batchSaveClusters(clusters);
if (!result) { if (!result) {
await cpfpRepository.$insertProgressMarker(height); await cpfpRepository.$insertProgressMarker(height);
} }

View File

@@ -1,4 +1,4 @@
import { Ancestor, CpfpInfo, CpfpSummary, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } 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';
@@ -57,11 +57,11 @@ export class Common {
return arr; return arr;
} }
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended[] } { static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
const matches: { [txid: string]: TransactionExtended[] } = {}; const matches: { [txid: string]: TransactionExtended } = {};
added deleted
.forEach((addedTx) => { .forEach((deletedTx) => {
const foundMatches = deleted.filter((deletedTx) => { const foundMatches = added.find((addedTx) => {
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx. // The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
return addedTx.fee > deletedTx.fee return addedTx.fee > deletedTx.fee
// The new transaction must pay more fee per kB than the replaced tx. // The new transaction must pay more fee per kB than the replaced tx.
@@ -70,8 +70,8 @@ export class Common {
&& deletedTx.vin.some((deletedVin) => && deletedTx.vin.some((deletedVin) =>
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
}); });
if (foundMatches?.length) { if (foundMatches) {
matches[addedTx.txid] = foundMatches; matches[deletedTx.txid] = foundMatches;
} }
}); });
return matches; return matches;
@@ -83,7 +83,6 @@ export class Common {
fee: tx.fee, fee: tx.fee,
vsize: tx.weight / 4, vsize: tx.weight / 4,
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0), value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
rate: tx.effectiveFeePerVsize,
}; };
} }
@@ -346,99 +345,4 @@ export class Common {
}; };
} }
} }
static calculateCpfp(height: number, transactions: TransactionExtended[]): CpfpSummary {
const clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[] = [];
let cluster: TransactionExtended[] = [];
let ancestors: { [txid: string]: boolean } = {};
const txMap = {};
for (let i = transactions.length - 1; i >= 0; i--) {
const tx = transactions[i];
txMap[tx.txid] = tx;
if (!ancestors[tx.txid]) {
let totalFee = 0;
let totalVSize = 0;
cluster.forEach(tx => {
totalFee += tx?.fee || 0;
totalVSize += (tx.weight / 4);
});
const effectiveFeePerVsize = totalFee / totalVSize;
if (cluster.length > 1) {
clusters.push({
root: cluster[0].txid,
height,
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.weight, fee: tx.fee || 0 }; }),
effectiveFeePerVsize,
});
}
cluster.forEach(tx => {
txMap[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
});
cluster = [];
ancestors = {};
}
cluster.push(tx);
tx.vin.forEach(vin => {
ancestors[vin.txid] = true;
});
}
return {
transactions,
clusters,
};
}
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats {
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
let weightCount = 0;
let medianFee = 0;
let medianWeight = 0;
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
const leftBound = 1995000;
const rightBound = 2005000;
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
const left = weightCount;
const right = weightCount + sortedTxs[i].weight;
if (right > leftBound) {
const weight = Math.min(right, rightBound) - Math.max(left, leftBound);
medianFee += (sortedTxs[i].rate * (weight / 4) );
medianWeight += weight;
}
weightCount += sortedTxs[i].weight;
}
const medianFeeRate = medianWeight ? (medianFee / (medianWeight / 4)) : 0;
// minimum effective fee heuristic:
// lowest of
// a) the 1st percentile of effective fee rates
// b) the minimum effective fee rate in the last 2% of transactions (in block order)
const minFee = Math.min(
Common.getNthPercentile(1, sortedTxs).rate,
transactions.slice(-transactions.length / 50).reduce((min, tx) => { return Math.min(min, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, Infinity)
);
// maximum effective fee heuristic:
// highest of
// a) the 99th percentile of effective fee rates
// b) the maximum effective fee rate in the first 2% of transactions (in block order)
const maxFee = Math.max(
Common.getNthPercentile(99, sortedTxs).rate,
transactions.slice(0, transactions.length / 50).reduce((max, tx) => { return Math.max(max, tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4))); }, 0)
);
return {
medianFee: medianFeeRate,
feeRange: [
minFee,
[10,25,50,75,90].map(n => Common.getNthPercentile(n, sortedTxs).rate),
maxFee,
].flat(),
};
}
static getNthPercentile(n: number, sortedDistribution: any[]): any {
return sortedDistribution[Math.floor((sortedDistribution.length - 1) * (n / 100))];
}
} }

View File

@@ -497,7 +497,6 @@ class DatabaseMigration {
this.uniqueLog(logger.notice, this.blocksTruncatedMessage); this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('DELETE FROM `pools`'); await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
this.uniqueLog(logger.notice, '`pools` table has been truncated`'); this.uniqueLog(logger.notice, '`pools` table has been truncated`');
await this.updateToSchemaVersion(56); await this.updateToSchemaVersion(56);
} }

View File

@@ -24,11 +24,12 @@ export function calcDifficultyAdjustment(
network: string, network: string,
latestBlockTimestamp: number, latestBlockTimestamp: number,
): DifficultyAdjustment { ): DifficultyAdjustment {
const ESTIMATE_LAG_BLOCKS = 146; // For first 7.2% of epoch, don't estimate.
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet const BLOCK_SECONDS_TARGET = 600; // Bitcoin mainnet
const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet const TESTNET_MAX_BLOCK_SECONDS = 1200; // Bitcoin testnet
const diffSeconds = Math.max(0, nowSeconds - DATime); const diffSeconds = nowSeconds - DATime;
const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0; const blocksInEpoch = (blockHeight >= 0) ? blockHeight % EPOCH_BLOCK_LENGTH : 0;
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100; const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch; const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
@@ -36,16 +37,18 @@ export function calcDifficultyAdjustment(
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET; const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
let difficultyChange = 0; let difficultyChange = 0;
let timeAvgSecs = blocksInEpoch ? diffSeconds / blocksInEpoch : BLOCK_SECONDS_TARGET; let timeAvgSecs = diffSeconds / blocksInEpoch;
// Only calculate the estimate once we have 7.2% of blocks in current epoch
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100; if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
// Max increase is x4 (+300%) difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
if (difficultyChange > 300) { // Max increase is x4 (+300%)
difficultyChange = 300; if (difficultyChange > 300) {
} difficultyChange = 300;
// Max decrease is /4 (-75%) }
if (difficultyChange < -75) { // Max decrease is /4 (-75%)
difficultyChange = -75; if (difficultyChange < -75) {
difficultyChange = -75;
}
} }
// Testnet difficulty is set to 1 after 20 minutes of no blocks, // Testnet difficulty is set to 1 after 20 minutes of no blocks,

View File

@@ -7,37 +7,32 @@ import logger from '../logger';
import config from '../config'; import config from '../config';
import { TransactionExtended } from '../mempool.interfaces'; import { TransactionExtended } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import rbfCache from './rbf-cache';
class DiskCache { class DiskCache {
private cacheSchemaVersion = 3; private cacheSchemaVersion = 3;
private rbfCacheSchemaVersion = 1;
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json'; private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json'; private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json'; private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json'; private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static TMP_RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-rbfcache.json';
private static RBF_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/rbfcache.json';
private static CHUNK_FILES = 25; private static CHUNK_FILES = 25;
private isWritingCache = false; private isWritingCache = false;
private semaphore: { resume: (() => void)[], locks: number } = {
resume: [],
locks: 0,
};
constructor() { constructor() {
if (!cluster.isPrimary) { if (!cluster.isMaster) {
return; return;
} }
process.on('SIGINT', (e) => { process.on('SIGINT', (e) => {
this.$saveCacheToDisk(true); this.saveCacheToDiskSync();
process.exit(0); process.exit(2);
});
process.on('SIGTERM', (e) => {
this.saveCacheToDiskSync();
process.exit(2);
}); });
} }
async $saveCacheToDisk(sync: boolean = false): Promise<void> { async $saveCacheToDisk(): Promise<void> {
if (!cluster.isPrimary) { if (!cluster.isPrimary) {
return; return;
} }
@@ -46,95 +41,87 @@ class DiskCache {
return; return;
} }
try { try {
logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`); logger.debug('Writing mempool and blocks data to disk cache (async)...');
this.isWritingCache = true; this.isWritingCache = true;
const mempool = memPool.getMempool(); const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = []; const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) { for (const tx in mempool) {
if (mempool[tx]) { mempoolArray.push(mempool[tx]);
mempoolArray.push(mempool[tx]);
}
} }
Common.shuffleArray(mempoolArray); Common.shuffleArray(mempoolArray);
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES); const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
if (sync) { await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({ network: config.MEMPOOL.NETWORK,
network: config.MEMPOOL.NETWORK, cacheSchemaVersion: this.cacheSchemaVersion,
cacheSchemaVersion: this.cacheSchemaVersion, blocks: blocks.getBlocks(),
blocks: blocks.getBlocks(), blockSummaries: blocks.getBlockSummaries(),
blockSummaries: blocks.getBlockSummaries(), mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {}, mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize), mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' }); }), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} else {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await this.$yield();
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
} }
logger.debug('Mempool and blocks data saved to disk cache'); logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false; this.isWritingCache = false;
} catch (e) { } catch (e) {
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e)); logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
this.isWritingCache = false; this.isWritingCache = false;
} }
}
saveCacheToDiskSync(): void {
if (!cluster.isPrimary) {
return;
}
if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.');
return;
}
try { try {
logger.debug('Writing rbf data to disk cache (async)...'); logger.debug('Writing mempool and blocks data to disk cache (sync)...');
this.isWritingCache = true; this.isWritingCache = true;
const rbfData = rbfCache.dump();
if (sync) { const mempool = memPool.getMempool();
fs.writeFileSync(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({ const mempoolArray: TransactionExtended[] = [];
network: config.MEMPOOL.NETWORK, for (const tx in mempool) {
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion, mempoolArray.push(mempool[tx]);
rbf: rbfData,
}), { flag: 'w' });
fs.renameSync(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
} else {
await fsPromises.writeFile(DiskCache.TMP_RBF_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
rbfCacheSchemaVersion: this.rbfCacheSchemaVersion,
rbf: rbfData,
}), { flag: 'w' });
await fsPromises.rename(DiskCache.TMP_RBF_FILE_NAME, DiskCache.RBF_FILE_NAME);
} }
logger.debug('Rbf data saved to disk cache');
Common.shuffleArray(mempoolArray);
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
}), { flag: 'w' });
}
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
logger.debug('Mempool and blocks data saved to disk cache');
this.isWritingCache = false; this.isWritingCache = false;
} catch (e) { } catch (e) {
logger.warn('Error writing rbf data to cache file: ' + (e instanceof Error ? e.message : e)); logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
this.isWritingCache = false; this.isWritingCache = false;
} }
} }
@@ -161,19 +148,7 @@ class DiskCache {
} }
} }
wipeRbfCache() { loadMempoolCache(): void {
logger.notice(`Wipping nodejs backend cache/rbfcache.json file`);
try {
fs.unlinkSync(DiskCache.RBF_FILE_NAME);
} catch (e: any) {
if (e?.code !== 'ENOENT') {
logger.err(`Cannot wipe cache file ${DiskCache.RBF_FILE_NAME}. Exception ${JSON.stringify(e)}`);
}
}
}
async $loadMempoolCache(): Promise<void> {
if (!fs.existsSync(DiskCache.FILE_NAME)) { if (!fs.existsSync(DiskCache.FILE_NAME)) {
return; return;
} }
@@ -213,65 +188,16 @@ class DiskCache {
} }
} }
} catch (e) { } catch (e) {
logger.err('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
} }
} }
await memPool.$setMempool(data.mempool); memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks); blocks.setBlocks(data.blocks);
blocks.setBlockSummaries(data.blockSummaries || []); blocks.setBlockSummaries(data.blockSummaries || []);
} catch (e) { } catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
} }
try {
let rbfData: any = {};
const rbfCacheData = fs.readFileSync(DiskCache.RBF_FILE_NAME, 'utf8');
if (rbfCacheData) {
logger.info('Restoring rbf data from disk cache');
rbfData = JSON.parse(rbfCacheData);
if (rbfData.rbfCacheSchemaVersion === undefined || rbfData.rbfCacheSchemaVersion !== this.rbfCacheSchemaVersion) {
logger.notice('Rbf disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeRbfCache();
}
if (rbfData.network && rbfData.network !== config.MEMPOOL.NETWORK) {
logger.notice('Rbf disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeRbfCache();
}
}
if (rbfData?.rbf) {
rbfCache.load(rbfData.rbf);
}
} catch (e) {
logger.warn('Failed to parse rbf cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
}
}
private $yield(): Promise<void> {
if (this.semaphore.locks) {
logger.debug('Pause writing mempool and blocks data to disk cache (async)');
return new Promise((resolve) => {
this.semaphore.resume.push(resolve);
});
} else {
return Promise.resolve();
}
}
public lock(): void {
this.semaphore.locks++;
}
public unlock(): void {
this.semaphore.locks = Math.max(0, this.semaphore.locks - 1);
if (!this.semaphore.locks && this.semaphore.resume.length) {
const nextResume = this.semaphore.resume.shift();
if (nextResume) {
logger.debug('Resume writing mempool and blocks data to disk cache (async)');
nextResume();
}
}
} }
} }

View File

@@ -4,29 +4,21 @@ import * as fs from 'fs';
import { AbstractLightningApi } from '../lightning-api-abstract-factory'; import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface'; import { ILightningApi } from '../lightning-api.interface';
import config from '../../../config'; import config from '../../../config';
import logger from '../../../logger';
class LndApi implements AbstractLightningApi { class LndApi implements AbstractLightningApi {
axiosConfig: AxiosRequestConfig = {}; axiosConfig: AxiosRequestConfig = {};
constructor() { constructor() {
if (!config.LIGHTNING.ENABLED) { if (config.LIGHTNING.ENABLED) {
return;
}
try {
this.axiosConfig = { this.axiosConfig = {
headers: { headers: {
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex'), 'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex')
}, },
httpsAgent: new Agent({ httpsAgent: new Agent({
ca: fs.readFileSync(config.LND.TLS_CERT_PATH) ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
}), }),
timeout: config.LND.TIMEOUT timeout: 10000
}; };
} catch (e) {
config.LIGHTNING.ENABLED = false;
logger.updateNetwork();
logger.err(`Could not initialize LND Macaroon/TLS Cert. Disabling LIGHTNING. ` + (e instanceof Error ? e.message : e));
} }
} }

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs';
import logger from '../../logger'; import logger from '../../logger';
class Icons { class Icons {
private static FILE_NAME = '/elements/asset_registry_db/icons.json'; private static FILE_NAME = './icons.json';
private iconIds: string[] = []; private iconIds: string[] = [];
private icons: { [assetId: string]: string; } = {}; private icons: { [assetId: string]: string; } = {};

View File

@@ -1,5 +1,5 @@
import logger from '../logger'; import logger from '../logger';
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces'; import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
@@ -10,9 +10,6 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = []; private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null; private txSelectionWorker: Worker | null = null;
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
constructor() {} constructor() {}
public getMempoolBlocks(): MempoolBlock[] { public getMempoolBlocks(): MempoolBlock[] {
@@ -57,19 +54,12 @@ class MempoolBlocks {
}); });
// First sort // First sort
memPoolArray.sort((a, b) => { memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
if (a.feePerVsize === b.feePerVsize) {
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return b.feePerVsize - a.feePerVsize;
}
});
// Loop through and traverse all ancestors and sum up all the sizes + fees // Loop through and traverse all ancestors and sum up all the sizes + fees
// Pass down size + fee to all unconfirmed children // Pass down size + fee to all unconfirmed children
let sizes = 0; let sizes = 0;
memPoolArray.forEach((tx) => { memPoolArray.forEach((tx, i) => {
sizes += tx.weight; sizes += tx.weight;
if (sizes > 4000000 * 8) { if (sizes > 4000000 * 8) {
return; return;
@@ -78,20 +68,13 @@ class MempoolBlocks {
}); });
// Final sort, by effective fee // Final sort, by effective fee
memPoolArray.sort((a, b) => { memPoolArray.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
if (a.effectiveFeePerVsize === b.effectiveFeePerVsize) {
// tie-break by lexicographic txid order for stability
return a.txid < b.txid ? -1 : 1;
} else {
return b.effectiveFeePerVsize - a.effectiveFeePerVsize;
}
});
const end = new Date().getTime(); const end = new Date().getTime();
const time = end - start; const time = end - start;
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
const blocks = this.calculateMempoolBlocks(memPoolArray); const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks); const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
@@ -102,47 +85,26 @@ class MempoolBlocks {
return blocks; return blocks;
} }
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] { private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
const mempoolBlocks: MempoolBlockWithTransactions[] = []; const mempoolBlocks: MempoolBlockWithTransactions[] = [];
let blockSize = 0;
let blockWeight = 0; let blockWeight = 0;
let blockVsize = 0; let blockSize = 0;
let blockFees = 0;
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
let transactionIds: string[] = [];
let transactions: TransactionExtended[] = []; let transactions: TransactionExtended[] = [];
transactionsSorted.forEach((tx) => { transactionsSorted.forEach((tx) => {
if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS if (blockWeight + tx.weight <= config.MEMPOOL.BLOCK_WEIGHT_UNITS
|| mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) { || mempoolBlocks.length === config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT - 1) {
tx.position = {
block: mempoolBlocks.length,
vsize: blockVsize + (tx.vsize / 2),
};
blockWeight += tx.weight; blockWeight += tx.weight;
blockVsize += tx.vsize;
blockSize += tx.size; blockSize += tx.size;
blockFees += tx.fee; transactions.push(tx);
if (blockVsize <= sizeLimit) {
transactions.push(tx);
}
transactionIds.push(tx.txid);
} else { } else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
blockVsize = 0;
tx.position = {
block: mempoolBlocks.length,
vsize: blockVsize + (tx.vsize / 2),
};
blockVsize += tx.vsize;
blockWeight = tx.weight; blockWeight = tx.weight;
blockSize = tx.size; blockSize = tx.size;
blockFees = tx.fee;
transactionIds = [tx.txid];
transactions = [tx]; transactions = [tx];
} }
}); });
if (transactions.length) { if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactionIds, transactions, blockSize, blockWeight, blockFees)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length));
} }
return mempoolBlocks; return mempoolBlocks;
@@ -153,7 +115,6 @@ class MempoolBlocks {
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) { for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = []; let added: TransactionStripped[] = [];
let removed: string[] = []; let removed: string[] = [];
const changed: { txid: string, rate: number | undefined }[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) { if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions; added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) { } else if (!mempoolBlocks[i] && prevBlocks[i]) {
@@ -162,7 +123,7 @@ class MempoolBlocks {
const prevIds = {}; const prevIds = {};
const newIds = {}; const newIds = {};
prevBlocks[i].transactions.forEach(tx => { prevBlocks[i].transactions.forEach(tx => {
prevIds[tx.txid] = tx; prevIds[tx.txid] = true;
}); });
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
newIds[tx.txid] = true; newIds[tx.txid] = true;
@@ -175,43 +136,30 @@ class MempoolBlocks {
mempoolBlocks[i].transactions.forEach(tx => { mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) { if (!prevIds[tx.txid]) {
added.push(tx); added.push(tx);
} else if (tx.rate !== prevIds[tx.txid].rate) {
changed.push({ txid: tx.txid, rate: tx.rate });
} }
}); });
} }
mempoolBlockDeltas.push({ mempoolBlockDeltas.push({
added, added,
removed, removed
changed,
}); });
} }
return mempoolBlockDeltas; return mempoolBlockDeltas;
} }
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
const start = Date.now();
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx);
}
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const strippedMempool: Map<number, CompactThreadTransaction> = new Map(); const strippedMempool: { [txid: string]: ThreadTransaction } = {};
Object.values(newMempool).forEach(entry => { Object.values(newMempool).forEach(entry => {
if (entry.uid != null) { strippedMempool[entry.txid] = {
strippedMempool.set(entry.uid, { txid: entry.txid,
uid: entry.uid, fee: entry.fee,
fee: entry.fee, weight: entry.weight,
weight: entry.weight, feePerVsize: entry.fee / (entry.weight / 4),
feePerVsize: entry.fee / (entry.weight / 4), effectiveFeePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)), vin: entry.vin.map(v => v.txid),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[], };
});
}
}); });
// (re)initialize tx selection worker thread // (re)initialize tx selection worker thread
@@ -230,7 +178,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
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
@@ -238,151 +186,103 @@ class MempoolBlocks {
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool }); this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise); const { blocks, clusters } = await workerResultPromise;
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
const processed = this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults); return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
return processed;
} catch (e) { } catch (e) {
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
} }
return this.mempoolBlocks; return this.mempoolBlocks;
} }
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> { public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
if (!this.txSelectionWorker) { if (!this.txSelectionWorker) {
// need to reset the worker // need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults); this.makeBlockTemplates(newMempool, saveResults);
return; return;
} }
const start = Date.now();
for (const tx of Object.values(added)) {
this.setUid(tx);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => { const addedStripped: ThreadTransaction[] = added.map(entry => {
return { return {
uid: entry.uid || 0, txid: entry.txid,
fee: entry.fee, fee: entry.fee,
weight: entry.weight, weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4), feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.effectiveFeePerVsize || (entry.fee / (entry.weight / 4)), effectiveFeePerVsize: entry.fee / (entry.weight / 4),
inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[], vin: entry.vin.map(v => v.txid),
}; };
}); });
// 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
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
}); });
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids }); this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
const { blocks, rates, clusters } = this.convertResultTxids(await workerResultPromise); const { blocks, clusters } = await workerResultPromise;
this.removeUids(removedUids);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, rates, clusters, saveResults); this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
} catch (e) { } catch (e) {
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
} }
} }
private processBlockTemplates(mempool, blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
for (const txid of Object.keys(rates)) {
if (txid in mempool) {
mempool[txid].effectiveFeePerVsize = rates[txid];
}
}
const readyBlocks: { transactionIds, transactions, totalSize, totalWeight, totalFees }[] = [];
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
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { blocks.forEach(block => {
const block: string[] = blocks[blockIndex]; block.forEach(tx => {
let txid: string; if (tx.txid in mempool) {
let mempoolTx: TransactionExtended; if (tx.effectiveFeePerVsize != null) {
let totalSize = 0; mempool[tx.txid].effectiveFeePerVsize = tx.effectiveFeePerVsize;
let totalVsize = 0;
let totalWeight = 0;
let totalFees = 0;
const transactions: TransactionExtended[] = [];
for (let txIndex = 0; txIndex < block.length; txIndex++) {
txid = block[txIndex];
if (txid) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
block: blockIndex,
vsize: totalVsize + (mempoolTx.vsize / 2),
};
mempoolTx.cpfpChecked = true;
totalSize += mempoolTx.size;
totalVsize += mempoolTx.vsize;
totalWeight += mempoolTx.weight;
totalFees += mempoolTx.fee;
if (totalVsize <= sizeLimit) {
transactions.push(mempoolTx);
} }
} if (tx.cpfpRoot && tx.cpfpRoot in clusters) {
} const ancestors: Ancestor[] = [];
readyBlocks.push({ const descendants: Ancestor[] = [];
transactionIds: block, const cluster = clusters[tx.cpfpRoot];
transactions, let matched = false;
totalSize, cluster.forEach(txid => {
totalWeight, if (txid === tx.txid) {
totalFees matched = true;
});
}
for (const cluster of Object.values(clusters)) {
for (const memberTxid of cluster) {
if (memberTxid in mempool) {
const mempoolTx = mempool[memberTxid];
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let matched = false;
cluster.forEach(txid => {
if (txid === memberTxid) {
matched = true;
} else {
const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else { } else {
ancestors.push(relative); const relative = {
txid: txid,
fee: mempool[txid].fee,
weight: mempool[txid].weight,
};
if (matched) {
descendants.push(relative);
} else {
ancestors.push(relative);
}
} }
} });
}); mempool[tx.txid].ancestors = ancestors;
mempoolTx.ancestors = ancestors; mempool[tx.txid].descendants = descendants;
mempoolTx.descendants = descendants; mempool[tx.txid].bestDescendant = null;
mempoolTx.bestDescendant = null; }
mempool[tx.txid].cpfpChecked = tx.cpfpChecked;
} }
} });
} });
const mempoolBlocks = readyBlocks.map(b => this.dataToMempoolBlocks(b.transactionIds, b.transactions, b.totalSize, b.totalWeight, b.totalFees)); // unpack the condensed blocks into proper mempool blocks
const mempoolBlocks = blocks.map((transactions, blockIndex) => {
return this.dataToMempoolBlocks(transactions.map(tx => {
return mempool[tx.txid] || null;
}).filter(tx => !!tx), blockIndex);
});
if (saveResults) { if (saveResults) {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
@@ -393,69 +293,37 @@ class MempoolBlocks {
return mempoolBlocks; return mempoolBlocks;
} }
private dataToMempoolBlocks(transactionIds: string[], transactions: TransactionExtended[], totalSize: number, totalWeight: number, totalFees: number): MempoolBlockWithTransactions { private dataToMempoolBlocks(transactions: TransactionExtended[], blocksIndex: number): MempoolBlockWithTransactions {
const feeStats = Common.calcEffectiveFeeStatistics(transactions); let totalSize = 0;
let totalWeight = 0;
const fitTransactions: TransactionExtended[] = [];
transactions.forEach(tx => {
totalSize += tx.size;
totalWeight += tx.weight;
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) {
fitTransactions.push(tx);
}
});
let rangeLength = 4;
if (blocksIndex === 0) {
rangeLength = 8;
}
if (transactions.length > 4000) {
rangeLength = 6;
} else if (transactions.length > 10000) {
rangeLength = 8;
}
return { return {
blockSize: totalSize, blockSize: totalSize,
blockVSize: (totalWeight / 4), // fractional vsize to avoid rounding errors blockVSize: totalWeight / 4,
nTx: transactionIds.length, nTx: transactions.length,
totalFees: totalFees, totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
medianFee: feeStats.medianFee, // Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE), medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: feeStats.feeRange, //Common.getFeesInRange(transactions, rangeLength), feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactionIds, transactionIds: transactions.map((tx) => tx.txid),
transactions: transactions.map((tx) => Common.stripTransaction(tx)), transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
}; };
} }
private resetUids(): void {
this.uidMap.clear();
this.nextUid = 1;
}
private setUid(tx: TransactionExtended): number {
const uid = this.nextUid;
this.nextUid++;
this.uidMap.set(uid, tx.txid);
tx.uid = uid;
return uid;
}
private getUid(tx: TransactionExtended): number | void {
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
return tx.uid;
}
}
private removeUids(uids: number[]): void {
for (const uid of uids) {
this.uidMap.delete(uid);
}
}
private convertResultTxids({ blocks, rates, clusters }: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]>})
: { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }} {
const convertedBlocks: string[][] = blocks.map(block => block.map(uid => {
return this.uidMap.get(uid) || '';
}));
const convertedRates = {};
for (const rateUid of rates.keys()) {
const rateTxid = this.uidMap.get(rateUid);
if (rateTxid) {
convertedRates[rateTxid] = rates.get(rateUid);
}
}
const convertedClusters = {};
for (const rootUid of clusters.keys()) {
const rootTxid = this.uidMap.get(rootUid);
if (rootTxid) {
const members = clusters.get(rootUid)?.map(uid => {
return this.uidMap.get(uid);
});
convertedClusters[rootTxid] = members;
}
}
return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }};
}
} }
export default new MempoolBlocks(); export default new MempoolBlocks();

View File

@@ -11,6 +11,8 @@ import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
class Mempool { class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false; private inSync: boolean = false;
private mempoolCacheDelta: number = -1; private mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {}; private mempoolCache: { [txId: string]: TransactionExtended } = {};
@@ -18,7 +20,7 @@ class Mempool {
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined; deletedTransactions: TransactionExtended[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined; deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
private txPerSecondArray: number[] = []; private txPerSecondArray: number[] = [];
@@ -33,10 +35,10 @@ class Mempool {
private SAMPLE_TIME = 10000; // In ms private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime(); private timer = new Date().getTime();
private missingTxCount = 0; private missingTxCount = 0;
private mainLoopTimeout: number = 120000;
constructor() { constructor() {
setInterval(this.updateTxPerSecond.bind(this), 1000); setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
} }
/** /**
@@ -70,20 +72,20 @@ class Mempool {
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; }, public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) { newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => Promise<void>) {
this.$asyncMempoolChangedCallback = fn; this.asyncMempoolChangedCallback = fn;
} }
public getMempool(): { [txid: string]: TransactionExtended } { public getMempool(): { [txid: string]: TransactionExtended } {
return this.mempoolCache; return this.mempoolCache;
} }
public async $setMempool(mempoolData: { [txId: string]: TransactionExtended }) { public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
this.mempoolCache = mempoolData; this.mempoolCache = mempoolData;
if (this.mempoolChangedCallback) { if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []); this.mempoolChangedCallback(this.mempoolCache, [], []);
} }
if (this.$asyncMempoolChangedCallback) { if (this.asyncMempoolChangedCallback) {
await this.$asyncMempoolChangedCallback(this.mempoolCache, [], []); this.asyncMempoolChangedCallback(this.mempoolCache, [], []);
} }
} }
@@ -116,23 +118,19 @@ class Mempool {
return txTimes; return txTimes;
} }
public async $updateMempool(transactions: string[]): Promise<void> { public async $updateMempool(): Promise<void> {
logger.debug(`Updating mempool...`); logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
const timer = this.startTimer();
const start = new Date().getTime(); const start = new Date().getTime();
let hasChange: boolean = false; let hasChange: boolean = false;
const currentMempoolSize = Object.keys(this.mempoolCache).length; const currentMempoolSize = Object.keys(this.mempoolCache).length;
this.updateTimerProgress(timer, 'got raw mempool'); const transactions = await bitcoinApi.$getRawMempool();
const diff = transactions.length - currentMempoolSize; const diff = transactions.length - currentMempoolSize;
const newTransactions: TransactionExtended[] = []; const newTransactions: TransactionExtended[] = [];
this.mempoolCacheDelta = Math.abs(diff); this.mempoolCacheDelta = Math.abs(diff);
if (!this.inSync) { if (!this.inSync) {
loadingIndicators.setProgress('mempool', currentMempoolSize / transactions.length * 100); loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
} }
// https://github.com/mempool/mempool/issues/3283 // https://github.com/mempool/mempool/issues/3283
@@ -145,12 +143,10 @@ class Mempool {
} }
}; };
let loggerTimer = new Date().getTime() / 1000;
for (const txid of transactions) { for (const txid of transactions) {
if (!this.mempoolCache[txid]) { if (!this.mempoolCache[txid]) {
try { try {
const transaction = await transactionUtils.$getTransactionExtended(txid); const transaction = await transactionUtils.$getTransactionExtended(txid);
this.updateTimerProgress(timer, 'fetched new transaction');
this.mempoolCache[txid] = transaction; this.mempoolCache[txid] = transaction;
if (this.inSync) { if (this.inSync) {
this.txPerSecondArray.push(new Date().getTime()); this.txPerSecondArray.push(new Date().getTime());
@@ -168,12 +164,9 @@ class Mempool {
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
} }
} }
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > 4) { if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100; break;
logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`);
loadingIndicators.setProgress('mempool', progress);
loggerTimer = new Date().getTime() / 1000;
} }
} }
@@ -207,15 +200,13 @@ class Mempool {
const transactionsObject = {}; const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true); transactions.forEach((txId) => transactionsObject[txId] = true);
// Delete evicted transactions from mempool // Flag transactions for lazy deletion
for (const tx in this.mempoolCache) { for (const tx in this.mempoolCache) {
if (!transactionsObject[tx]) { if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) {
deletedTransactions.push(this.mempoolCache[tx]); deletedTransactions.push(this.mempoolCache[tx]);
this.mempoolCache[tx].deleteAfter = new Date().getTime() + Mempool.LAZY_DELETE_AFTER_SECONDS * 1000;
} }
} }
for (const tx of deletedTransactions) {
delete this.mempoolCache[tx.txid];
}
} }
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
@@ -232,46 +223,22 @@ class Mempool {
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions); this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
} }
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) { if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.updateTimerProgress(timer, 'running async mempool callback'); await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
await this.$asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
this.updateTimerProgress(timer, 'completed async mempool callback');
} }
const end = new Date().getTime(); const end = new Date().getTime();
const time = end - start; const time = end - start;
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`); logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
this.clearTimer(timer);
} }
private startTimer() { public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
const state: any = {
start: Date.now(),
progress: 'begin $updateMempool',
timer: null,
};
state.timer = setTimeout(() => {
logger.err(`$updateMempool stalled at "${state.progress}"`);
}, this.mainLoopTimeout);
return state;
}
private updateTimerProgress(state, msg) {
state.progress = msg;
}
private clearTimer(state) {
if (state.timer) {
clearTimeout(state.timer);
}
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended[]; }): void {
for (const rbfTransaction in rbfTransactions) { for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) { if (this.mempoolCache[rbfTransaction]) {
// Store replaced transactions // Store replaced transactions
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]); rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
} }
} }
} }
@@ -289,6 +256,17 @@ class Mempool {
} }
} }
private deleteExpiredTransactions() {
const now = new Date().getTime();
for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
if (lazyDeleteAt && lazyDeleteAt < now) {
delete this.mempoolCache[tx];
rbfCache.evict(tx);
}
}
}
private $getMempoolInfo() { private $getMempoolInfo() {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) { if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
return Promise.all([ return Promise.all([

View File

@@ -452,7 +452,7 @@ class Mining {
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) { if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100); const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.debug(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining); logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
} }
} }
@@ -558,10 +558,8 @@ class Mining {
currentBlockHeight -= 10000; currentBlockHeight -= 10000;
} }
if (totalIndexed > 0) { if (totalIndexed) {
logger.info(`Indexing missing coinstatsindex data completed. Indexed ${totalIndexed}`, logger.tags.mining); logger.info(`Indexing missing coinstatsindex data completed`, logger.tags.mining);
} else {
logger.debug(`Indexing missing coinstatsindex data completed. Indexed 0.`, logger.tags.mining);
} }
} }

View File

@@ -1,341 +1,65 @@
import logger from "../logger"; import { TransactionExtended } from "../mempool.interfaces";
import { TransactionExtended, TransactionStripped } from "../mempool.interfaces";
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { Common } from "./common";
interface RbfTransaction extends TransactionStripped {
rbf?: boolean;
mined?: boolean;
}
interface RbfTree {
tx: RbfTransaction;
time: number;
interval?: number;
mined?: boolean;
fullRbf: boolean;
replaces: RbfTree[];
}
class RbfCache { class RbfCache {
private replacedBy: Map<string, string> = new Map(); private replacedBy: { [txid: string]: string; } = {};
private replaces: Map<string, string[]> = new Map(); private replaces: { [txid: string]: string[] } = {};
private rbfTrees: Map<string, RbfTree> = new Map(); // sequences of consecutive replacements private txs: { [txid: string]: TransactionExtended } = {};
private dirtyTrees: Set<string> = new Set(); private expiring: { [txid: string]: Date } = {};
private treeMap: Map<string, string> = new Map(); // map of txids to sequence ids
private txs: Map<string, TransactionExtended> = new Map();
private expiring: Map<string, number> = new Map();
constructor() { constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10); setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
} }
public add(replaced: TransactionExtended[], newTxExtended: TransactionExtended): void { public add(replacedTx: TransactionExtended, newTxId: string): void {
if (!newTxExtended || !replaced?.length) { this.replacedBy[replacedTx.txid] = newTxId;
return; this.txs[replacedTx.txid] = replacedTx;
if (!this.replaces[newTxId]) {
this.replaces[newTxId] = [];
} }
this.replaces[newTxId].push(replacedTx.txid);
const newTx = Common.stripTransaction(newTxExtended) as RbfTransaction;
const newTime = newTxExtended.firstSeen || Date.now();
newTx.rbf = newTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.txs.set(newTx.txid, newTxExtended);
// maintain rbf trees
let fullRbf = false;
const replacedTrees: RbfTree[] = [];
for (const replacedTxExtended of replaced) {
const replacedTx = Common.stripTransaction(replacedTxExtended) as RbfTransaction;
replacedTx.rbf = replacedTxExtended.vin.some((v) => v.sequence < 0xfffffffe);
this.replacedBy.set(replacedTx.txid, newTx.txid);
if (this.treeMap.has(replacedTx.txid)) {
const treeId = this.treeMap.get(replacedTx.txid);
if (treeId) {
const tree = this.rbfTrees.get(treeId);
this.rbfTrees.delete(treeId);
if (tree) {
tree.interval = newTime - tree?.time;
replacedTrees.push(tree);
fullRbf = fullRbf || tree.fullRbf;
}
}
} else {
const replacedTime = replacedTxExtended.firstSeen || Date.now();
replacedTrees.push({
tx: replacedTx,
time: replacedTime,
interval: newTime - replacedTime,
fullRbf: !replacedTx.rbf,
replaces: [],
});
fullRbf = fullRbf || !replacedTx.rbf;
this.txs.set(replacedTx.txid, replacedTxExtended);
}
}
const treeId = replacedTrees[0].tx.txid;
const newTree = {
tx: newTx,
time: newTxExtended.firstSeen || Date.now(),
fullRbf,
replaces: replacedTrees
};
this.rbfTrees.set(treeId, newTree);
this.updateTreeMap(treeId, newTree);
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
this.dirtyTrees.add(treeId);
} }
public getReplacedBy(txId: string): string | undefined { public getReplacedBy(txId: string): string | undefined {
return this.replacedBy.get(txId); return this.replacedBy[txId];
} }
public getReplaces(txId: string): string[] | undefined { public getReplaces(txId: string): string[] | undefined {
return this.replaces.get(txId); return this.replaces[txId];
} }
public getTx(txId: string): TransactionExtended | undefined { public getTx(txId: string): TransactionExtended | undefined {
return this.txs.get(txId); return this.txs[txId];
}
public getRbfTree(txId: string): RbfTree | void {
return this.rbfTrees.get(this.treeMap.get(txId) || '');
}
// get a paginated list of RbfTrees
// ordered by most recent replacement time
public getRbfTrees(onlyFullRbf: boolean, after?: string): RbfTree[] {
const limit = 25;
const trees: RbfTree[] = [];
const used = new Set<string>();
const replacements: string[][] = Array.from(this.replacedBy).reverse();
const afterTree = after ? this.treeMap.get(after) : null;
let ready = !afterTree;
for (let i = 0; i < replacements.length && trees.length <= limit - 1; i++) {
const txid = replacements[i][1];
const treeId = this.treeMap.get(txid) || '';
if (treeId === afterTree) {
ready = true;
} else if (ready) {
if (!used.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
used.add(treeId);
if (tree && (!onlyFullRbf || tree.fullRbf)) {
trees.push(tree);
}
}
}
}
return trees;
}
// get map of rbf trees that have been updated since the last call
public getRbfChanges(): { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} {
const changes: { trees: {[id: string]: RbfTree }, map: { [txid: string]: string }} = {
trees: {},
map: {},
};
this.dirtyTrees.forEach(id => {
const tree = this.rbfTrees.get(id);
if (tree) {
changes.trees[id] = tree;
this.getTransactionsInTree(tree).forEach(tx => {
changes.map[tx.txid] = id;
});
}
});
this.dirtyTrees = new Set();
return changes;
}
public mined(txid): void {
if (!this.txs.has(txid)) {
return;
}
const treeId = this.treeMap.get(txid);
if (treeId && this.rbfTrees.has(treeId)) {
const tree = this.rbfTrees.get(treeId);
if (tree) {
this.setTreeMined(tree, txid);
tree.mined = true;
this.dirtyTrees.add(treeId);
}
}
this.evict(txid);
} }
// flag a transaction as removed from the mempool // flag a transaction as removed from the mempool
public evict(txid: string, fast: boolean = false): void { public evict(txid): void {
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
this.expiring.set(txid, fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400)); // 24 hours
}
} }
private cleanup(): void { private cleanup(): void {
const now = Date.now(); const currentDate = new Date();
for (const txid of this.expiring.keys()) { for (const txid in this.expiring) {
if ((this.expiring.get(txid) || 0) < now) { if (this.expiring[txid] < currentDate) {
this.expiring.delete(txid); delete this.expiring[txid];
this.remove(txid); this.remove(txid);
} }
} }
logger.debug(`rbf cache contains ${this.txs.size} txs, ${this.expiring.size} due to expire`);
} }
// remove a transaction & all previous versions from the cache // remove a transaction & all previous versions from the cache
private remove(txid): void { private remove(txid): void {
// don't remove a transaction if a newer version remains in the mempool // don't remove a transaction while a newer version remains in the mempool
if (!this.replacedBy.has(txid)) { if (this.replaces[txid] && !this.replacedBy[txid]) {
const replaces = this.replaces.get(txid); const replaces = this.replaces[txid];
this.replaces.delete(txid); delete this.replaces[txid];
this.treeMap.delete(txid); for (const tx of replaces) {
this.txs.delete(txid);
this.expiring.delete(txid);
for (const tx of (replaces || [])) {
// recursively remove prior versions from the cache // recursively remove prior versions from the cache
this.replacedBy.delete(tx); delete this.replacedBy[tx];
// if this is the id of a tree, remove that too delete this.txs[tx];
if (this.treeMap.get(tx) === tx) {
this.rbfTrees.delete(tx);
}
this.remove(tx); this.remove(tx);
} }
} }
} }
private updateTreeMap(newId: string, tree: RbfTree): void {
this.treeMap.set(tree.tx.txid, newId);
tree.replaces.forEach(subtree => {
this.updateTreeMap(newId, subtree);
});
}
private getTransactionsInTree(tree: RbfTree, txs: RbfTransaction[] = []): RbfTransaction[] {
txs.push(tree.tx);
tree.replaces.forEach(subtree => {
this.getTransactionsInTree(subtree, txs);
});
return txs;
}
private setTreeMined(tree: RbfTree, txid: string): void {
if (tree.tx.txid === txid) {
tree.tx.mined = true;
} else {
tree.replaces.forEach(subtree => {
this.setTreeMined(subtree, txid);
});
}
}
public dump(): any {
const trees = Array.from(this.rbfTrees.values()).map((tree: RbfTree) => { return this.exportTree(tree); });
return {
txs: Array.from(this.txs.entries()),
trees,
expiring: Array.from(this.expiring.entries()),
};
}
public async load({ txs, trees, expiring }): Promise<void> {
txs.forEach(txEntry => {
this.txs.set(txEntry[0], txEntry[1]);
});
for (const deflatedTree of trees) {
await this.importTree(deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
}
expiring.forEach(expiringEntry => {
if (this.txs.has(expiringEntry[0])) {
this.expiring.set(expiringEntry[0], new Date(expiringEntry[1]).getTime());
}
});
this.cleanup();
}
exportTree(tree: RbfTree, deflated: any = null) {
if (!deflated) {
deflated = {
root: tree.tx.txid,
};
}
deflated[tree.tx.txid] = {
tx: tree.tx.txid,
txMined: tree.tx.mined,
time: tree.time,
interval: tree.interval,
mined: tree.mined,
fullRbf: tree.fullRbf,
replaces: tree.replaces.map(child => child.tx.txid),
};
tree.replaces.forEach(child => {
this.exportTree(child, deflated);
});
return deflated;
}
async importTree(root, txid, deflated, txs: Map<string, TransactionExtended>, mined: boolean = false): Promise<RbfTree | void> {
const treeInfo = deflated[txid];
const replaces: RbfTree[] = [];
// check if any transactions in this tree have already been confirmed
mined = mined || treeInfo.mined;
let exists = mined;
if (!mined) {
try {
const apiTx = await bitcoinApi.$getRawTransaction(txid);
if (apiTx) {
exists = true;
}
if (apiTx?.status?.confirmed) {
mined = true;
treeInfo.txMined = true;
this.evict(txid, true);
}
} catch (e) {
// most transactions do not exist
}
}
// if the root tx is not in the mempool or the blockchain
// evict this tree as soon as possible
if (root === txid && !exists) {
this.evict(txid, true);
}
// recursively reconstruct child trees
for (const childId of treeInfo.replaces) {
const replaced = await this.importTree(root, childId, deflated, txs, mined);
if (replaced) {
this.replacedBy.set(replaced.tx.txid, txid);
replaces.push(replaced);
if (replaced.mined) {
mined = true;
}
}
}
this.replaces.set(txid, replaces.map(t => t.tx.txid));
const tx = txs.get(txid);
if (!tx) {
return;
}
const strippedTx = Common.stripTransaction(tx) as RbfTransaction;
strippedTx.rbf = tx.vin.some((v) => v.sequence < 0xfffffffe);
strippedTx.mined = treeInfo.txMined;
const tree = {
tx: strippedTx,
time: treeInfo.time,
interval: treeInfo.interval,
mined: mined,
fullRbf: treeInfo.fullRbf,
replaces,
};
this.treeMap.set(txid, root);
if (root === txid) {
this.rbfTrees.set(root, tree);
this.dirtyTrees.add(root);
}
return tree;
}
} }
export default new RbfCache(); export default new RbfCache();

View File

@@ -1,10 +1,11 @@
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { CompactThreadTransaction, AuditTransaction } from '../mempool.interfaces'; import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap'; import { PairingHeap } from '../utils/pairing-heap';
import { Common } from './common';
import { parentPort } from 'worker_threads'; import { parentPort } from 'worker_threads';
let mempool: Map<number, CompactThreadTransaction> = new Map(); let mempool: { [txid: string]: ThreadTransaction } = {};
if (parentPort) { if (parentPort) {
parentPort.on('message', (params) => { parentPort.on('message', (params) => {
@@ -12,18 +13,18 @@ if (parentPort) {
mempool = params.mempool; mempool = params.mempool;
} else if (params.type === 'update') { } else if (params.type === 'update') {
params.added.forEach(tx => { params.added.forEach(tx => {
mempool.set(tx.uid, tx); mempool[tx.txid] = tx;
}); });
params.removed.forEach(uid => { params.removed.forEach(txid => {
mempool.delete(uid); delete mempool[txid];
}); });
} }
const { blocks, rates, clusters } = makeBlockTemplates(mempool); const { blocks, clusters } = makeBlockTemplates(mempool);
// return the result to main thread. // return the result to main thread.
if (parentPort) { if (parentPort) {
parentPort.postMessage({ blocks, rates, clusters }); parentPort.postMessage({ blocks, clusters });
} }
}); });
} }
@@ -32,25 +33,26 @@ if (parentPort) {
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core * Build projected mempool blocks 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) * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/ */
function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>) function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
: { blocks: number[][], rates: Map<number, number>, clusters: Map<number, number[]> } { : { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } {
const start = Date.now(); const start = Date.now();
const auditPool: Map<number, AuditTransaction> = new Map(); const auditPool: { [txid: string]: AuditTransaction } = {};
const mempoolArray: AuditTransaction[] = []; const mempoolArray: AuditTransaction[] = [];
const cpfpClusters: Map<number, number[]> = new Map(); const restOfArray: ThreadTransaction[] = [];
const cpfpClusters: { [root: string]: string[] } = {};
mempool.forEach(tx => { // grab the top feerate txs up to maxWeight
tx.dirty = false; Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
// initializing everything up front helps V8 optimize property access later // initializing everything up front helps V8 optimize property access later
auditPool.set(tx.uid, { auditPool[tx.txid] = {
uid: tx.uid, txid: tx.txid,
fee: tx.fee, fee: tx.fee,
weight: tx.weight, weight: tx.weight,
feePerVsize: tx.feePerVsize, feePerVsize: tx.feePerVsize,
effectiveFeePerVsize: tx.feePerVsize, effectiveFeePerVsize: tx.feePerVsize,
inputs: tx.inputs || [], vin: tx.vin,
relativesSet: false, relativesSet: false,
ancestorMap: new Map<number, AuditTransaction>(), ancestorMap: new Map<string, AuditTransaction>(),
children: new Set<AuditTransaction>(), children: new Set<AuditTransaction>(),
ancestorFee: 0, ancestorFee: 0,
ancestorWeight: 0, ancestorWeight: 0,
@@ -58,8 +60,8 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
used: false, used: false,
modified: false, modified: false,
modifiedNode: null, modifiedNode: null,
}); };
mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction); mempoolArray.push(auditPool[tx.txid]);
}); });
// Build relatives graph & calculate ancestor scores // Build relatives graph & calculate ancestor scores
@@ -70,28 +72,15 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
} }
// Sort by descending ancestor score // Sort by descending ancestor score
mempoolArray.sort((a, b) => { mempoolArray.sort((a, b) => (b.score || 0) - (a.score || 0));
if (b.score === a.score) {
// tie-break by uid for stability
return a.uid < b.uid ? -1 : 1;
} else {
return (b.score || 0) - (a.score || 0);
}
});
// Build blocks by greedily choosing the highest feerate package // Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score) // (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: number[][] = []; const blocks: ThreadTransaction[][] = [];
let blockWeight = 4000; let blockWeight = 4000;
let blockSize = 0;
let transactions: AuditTransaction[] = []; let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => { const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => (a.score || 0) > (b.score || 0));
if (a.score === b.score) {
// tie-break by uid for stability
return a.uid > b.uid;
} else {
return (a.score || 0) > (b.score || 0);
}
});
let overflow: AuditTransaction[] = []; let overflow: AuditTransaction[] = [];
let failures = 0; let failures = 0;
let top = 0; let top = 0;
@@ -118,36 +107,30 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
if (nextTx && !nextTx?.used) { if (nextTx && !nextTx?.used) {
// Check if the package fits into this block // Check if the package fits into this block
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) { if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values()); const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count) // 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 sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
let isCluster = false; let isCluster = false;
if (sortedTxSet.length > 1) { if (sortedTxSet.length > 1) {
cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid)); cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid);
isCluster = true; isCluster = true;
} }
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
const used: AuditTransaction[] = []; const used: AuditTransaction[] = [];
while (sortedTxSet.length) { while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop(); const ancestor = sortedTxSet.pop();
const mempoolTx = mempool.get(ancestor.uid); const mempoolTx = mempool[ancestor.txid];
if (!mempoolTx) {
continue;
}
ancestor.used = true; ancestor.used = true;
ancestor.usedBy = nextTx.uid; ancestor.usedBy = nextTx.txid;
// update original copy of this tx with effective fee rate & relatives data // update original copy of this tx with effective fee rate & relatives data
if (mempoolTx.effectiveFeePerVsize !== effectiveFeeRate) { mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
mempoolTx.effectiveFeePerVsize = effectiveFeeRate; if (isCluster) {
mempoolTx.dirty = true; mempoolTx.cpfpRoot = nextTx.txid;
}
if (mempoolTx.cpfpRoot !== nextTx.uid) {
mempoolTx.cpfpRoot = isCluster ? nextTx.uid : null;
mempoolTx.dirty;
} }
mempoolTx.cpfpChecked = true; mempoolTx.cpfpChecked = true;
transactions.push(ancestor); transactions.push(ancestor);
blockSize += ancestor.size;
blockWeight += ancestor.weight; blockWeight += ancestor.weight;
used.push(ancestor); used.push(ancestor);
} }
@@ -173,10 +156,11 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) { if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
// construct this block // construct this block
if (transactions.length) { if (transactions.length) {
blocks.push(transactions.map(t => t.uid)); blocks.push(transactions.map(t => mempool[t.txid]));
} }
// reset for the next block // reset for the next block
transactions = []; transactions = [];
blockSize = 0;
blockWeight = 4000; blockWeight = 4000;
// 'overflow' packages didn't fit in this block, but are valid candidates for the next // 'overflow' packages didn't fit in this block, but are valid candidates for the next
@@ -191,38 +175,50 @@ function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
overflow = []; overflow = [];
} }
} }
// pack any leftover transactions into the last block
if (overflow.length > 0) { for (const tx of overflow) {
logger.warn('GBT overflow list unexpectedly non-empty after final block constructed'); if (!tx || tx?.used) {
} continue;
// add the final unbounded block if it contains any transactions
if (transactions.length > 0) {
blocks.push(transactions.map(t => t.uid));
}
// get map of dirty transactions
const rates = new Map<number, number>();
for (const tx of mempool.values()) {
if (tx?.dirty) {
rates.set(tx.uid, tx.effectiveFeePerVsize || tx.feePerVsize);
} }
blockWeight += tx.weight;
const mempoolTx = mempool[tx.txid];
// update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = tx.score;
if (tx.ancestorMap.size > 0) {
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
mempoolTx.cpfpRoot = tx.txid;
}
mempoolTx.cpfpChecked = true;
transactions.push(tx);
tx.used = true;
} }
const blockTransactions = transactions.map(t => mempool[t.txid]);
restOfArray.forEach(tx => {
blockWeight += tx.weight;
tx.effectiveFeePerVsize = tx.feePerVsize;
tx.cpfpChecked = false;
blockTransactions.push(tx);
});
if (blockTransactions.length) {
blocks.push(blockTransactions);
}
transactions = [];
const end = Date.now(); const end = Date.now();
const time = end - start; const time = end - start;
logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool templates calculated in ' + time / 1000 + ' seconds');
return { blocks, rates, clusters: cpfpClusters }; return { blocks, clusters: cpfpClusters };
} }
// traverse in-mempool ancestors // traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy // recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives( function setRelatives(
tx: AuditTransaction, tx: AuditTransaction,
mempool: Map<number, AuditTransaction>, mempool: { [txid: string]: AuditTransaction },
): void { ): void {
for (const parent of tx.inputs) { for (const parent of tx.vin) {
const parentTx = mempool.get(parent); const parentTx = mempool[parent];
if (parentTx && !tx.ancestorMap?.has(parent)) { if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx); tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx); parentTx.children.add(tx);
@@ -231,7 +227,7 @@ function setRelatives(
setRelatives(parentTx, mempool); setRelatives(parentTx, mempool);
} }
parentTx.ancestorMap.forEach((ancestor) => { parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.uid, ancestor); tx.ancestorMap.set(ancestor.txid, ancestor);
}); });
} }
}; };
@@ -249,7 +245,7 @@ function setRelatives(
// avoids recursion to limit call stack depth // avoids recursion to limit call stack depth
function updateDescendants( function updateDescendants(
rootTx: AuditTransaction, rootTx: AuditTransaction,
mempool: Map<number, AuditTransaction>, mempool: { [txid: string]: AuditTransaction },
modified: PairingHeap<AuditTransaction>, modified: PairingHeap<AuditTransaction>,
): void { ): void {
const descendantSet: Set<AuditTransaction> = new Set(); const descendantSet: Set<AuditTransaction> = new Set();
@@ -265,9 +261,9 @@ function updateDescendants(
}); });
while (descendants.length) { while (descendants.length) {
descendantTx = descendants.pop(); descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) { if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
// remove tx as ancestor // remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.uid); descendantTx.ancestorMap.delete(rootTx.txid);
descendantTx.ancestorFee -= rootTx.fee; descendantTx.ancestorFee -= rootTx.fee;
descendantTx.ancestorWeight -= rootTx.weight; descendantTx.ancestorWeight -= rootTx.weight;
tmpScore = descendantTx.score; tmpScore = descendantTx.score;

View File

@@ -26,10 +26,6 @@ class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
private extraInitProperties = {}; private extraInitProperties = {};
private numClients = 0;
private numConnected = 0;
private numDisconnected = 0;
constructor() { } constructor() { }
setWebsocketServer(wss: WebSocket.Server) { setWebsocketServer(wss: WebSocket.Server) {
@@ -46,11 +42,7 @@ class WebsocketHandler {
} }
this.wss.on('connection', (client: WebSocket) => { this.wss.on('connection', (client: WebSocket) => {
this.numConnected++;
client.on('error', logger.info); client.on('error', logger.info);
client.on('close', () => {
this.numDisconnected++;
});
client.on('message', async (message: string) => { client.on('message', async (message: string) => {
try { try {
const parsedMessage: WebsocketResponse = JSON.parse(message); const parsedMessage: WebsocketResponse = JSON.parse(message);
@@ -66,10 +58,9 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-tx']) { if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx']; client['track-tx'] = parsedMessage['track-tx'];
const trackTxid = client['track-tx'];
// Client is telling the transaction wasn't found // Client is telling the transaction wasn't found
if (parsedMessage['watch-mempool']) { if (parsedMessage['watch-mempool']) {
const rbfCacheTxid = rbfCache.getReplacedBy(trackTxid); const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
if (rbfCacheTxid) { if (rbfCacheTxid) {
response['txReplaced'] = { response['txReplaced'] = {
txid: rbfCacheTxid, txid: rbfCacheTxid,
@@ -77,7 +68,7 @@ class WebsocketHandler {
client['track-tx'] = null; client['track-tx'] = null;
} else { } else {
// It might have appeared before we had the time to start watching for it // It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[trackTxid]; const tx = memPool.getMempool()[client['track-tx']];
if (tx) { if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx; response['tx'] = tx;
@@ -101,13 +92,6 @@ class WebsocketHandler {
} }
} }
} }
const tx = memPool.getMempool()[trackTxid];
if (tx && tx.position) {
response['txPosition'] = {
txid: trackTxid,
position: tx.position,
};
}
} else { } else {
client['track-tx'] = null; client['track-tx'] = null;
} }
@@ -148,14 +132,6 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
client['track-rbf'] = parsedMessage['track-rbf'];
} else {
client['track-rbf'] = false;
}
}
if (parsedMessage.action === 'init') { if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
if (!_blocks) { if (!_blocks) {
@@ -235,7 +211,6 @@ class WebsocketHandler {
if (!_blocks) { if (!_blocks) {
_blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT); _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
} }
const da = difficultyAdjustment.getDifficultyAdjustment();
return { return {
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(), 'vBytesPerSecond': memPool.getVBytesPerSecond(),
@@ -245,7 +220,7 @@ class WebsocketHandler {
'transactions': memPool.getLatestTransactions(), 'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(), 'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined, 'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(), 'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties ...this.extraInitProperties
}; };
@@ -256,8 +231,6 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
this.wss.clients.forEach((client) => { this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) { if (client.readyState !== WebSocket.OPEN) {
return; return;
@@ -273,16 +246,14 @@ class WebsocketHandler {
}); });
} }
async $handleMempoolChange(newMempool: { [txid: string]: TransactionExtended }, async handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> { newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]): Promise<void> {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true); await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool, true);
} }
@@ -294,16 +265,6 @@ class WebsocketHandler {
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
let fullRbfReplacements;
if (Object.keys(rbfChanges.trees).length) {
rbfReplacements = rbfCache.getRbfTrees(false);
fullRbfReplacements = rbfCache.getRbfTrees(true);
}
for (const deletedTx of deletedTransactions) {
rbfCache.evict(deletedTx.txid);
}
const recommendedFees = feeApi.getRecommendedFee(); const recommendedFees = feeApi.getRecommendedFee();
this.wss.clients.forEach(async (client) => { this.wss.clients.forEach(async (client) => {
@@ -317,9 +278,7 @@ class WebsocketHandler {
response['mempoolInfo'] = mempoolInfo; response['mempoolInfo'] = mempoolInfo;
response['vBytesPerSecond'] = vBytesPerSecond; response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
if (da?.previousTime) { response['da'] = da;
response['da'] = da;
}
response['fees'] = recommendedFees; response['fees'] = recommendedFees;
} }
@@ -412,10 +371,9 @@ class WebsocketHandler {
} }
if (client['track-tx']) { if (client['track-tx']) {
const trackTxid = client['track-tx'];
const outspends: object = {}; const outspends: object = {};
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => { newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
if (vin.txid === trackTxid) { if (vin.txid === client['track-tx']) {
outspends[vin.vout] = { outspends[vin.vout] = {
vin: i, vin: i,
txid: tx.txid, txid: tx.txid,
@@ -427,25 +385,16 @@ class WebsocketHandler {
response['utxoSpent'] = outspends; response['utxoSpent'] = outspends;
} }
const rbfReplacedBy = rbfCache.getReplacedBy(client['track-tx']); if (rbfTransactions[client['track-tx']]) {
if (rbfReplacedBy) { for (const rbfTransaction in rbfTransactions) {
response['rbfTransaction'] = { if (client['track-tx'] === rbfTransaction) {
txid: rbfReplacedBy, response['rbfTransaction'] = {
txid: rbfTransactions[rbfTransaction].txid,
};
break;
}
} }
} }
const rbfChange = rbfChanges.map[client['track-tx']];
if (rbfChange) {
response['rbfInfo'] = rbfChanges.trees[rbfChange];
}
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = {
txid: trackTxid,
position: mempoolTx.position,
};
}
} }
if (client['track-mempool-block'] >= 0) { if (client['track-mempool-block'] >= 0) {
@@ -458,12 +407,6 @@ class WebsocketHandler {
} }
} }
if (client['track-rbf'] === 'all' && rbfReplacements) {
response['rbfLatest'] = rbfReplacements;
} else if (client['track-rbf'] === 'fullRbf' && fullRbfReplacements) {
response['rbfLatest'] = fullRbfReplacements;
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(JSON.stringify(response)); client.send(JSON.stringify(response));
} }
@@ -475,25 +418,17 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.printLogs();
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
if (config.MEMPOOL.AUDIT) { if (config.MEMPOOL.AUDIT) {
let projectedBlocks; let projectedBlocks;
let auditMempool = _memPool;
// template calculation functions have mempool side effects, so calculate audits using // template calculation functions have mempool side effects, so calculate audits using
// a cloned copy of the mempool if we're running a different algorithm for mempool updates // a cloned copy of the mempool if we're running a different algorithm for mempool updates
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL; const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool);
if (separateAudit) { if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
auditMempool = deepClone(_memPool); projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
} else { } else {
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
} }
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
@@ -539,14 +474,16 @@ class WebsocketHandler {
} }
} }
const removed: string[] = [];
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
for (const txId of txIds) { for (const txId of txIds) {
delete _memPool[txId]; delete _memPool[txId];
rbfCache.mined(txId); removed.push(txId);
rbfCache.evict(txId);
} }
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$makeBlockTemplates(_memPool, true); await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool, true); mempoolBlocks.updateMempoolBlocks(_memPool, true);
} }
@@ -568,7 +505,7 @@ class WebsocketHandler {
const response = { const response = {
'block': block, 'block': block,
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'da': da?.previousTime ? da : undefined, 'da': da,
'fees': fees, 'fees': fees,
}; };
@@ -576,19 +513,8 @@ class WebsocketHandler {
response['mempool-blocks'] = mBlocks; response['mempool-blocks'] = mBlocks;
} }
if (client['track-tx']) { if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
const trackTxid = client['track-tx']; response['txConfirmed'] = true;
if (txIds.indexOf(trackTxid) > -1) {
response['txConfirmed'] = true;
} else {
const mempoolTx = _memPool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = {
txid: trackTxid,
position: mempoolTx.position,
};
}
}
} }
if (client['track-address']) { if (client['track-address']) {
@@ -668,17 +594,6 @@ class WebsocketHandler {
client.send(JSON.stringify(response)); client.send(JSON.stringify(response));
}); });
} }
private printLogs(): void {
if (this.wss) {
const count = this.wss?.clients?.size || 0;
const diff = count - this.numClients;
this.numClients = count;
logger.debug(`${count} websocket clients | ${this.numConnected} connected | ${this.numDisconnected} disconnected | (${diff >= 0 ? '+' : ''}${diff})`);
this.numConnected = 0;
this.numDisconnected = 0;
}
}
} }
export default new WebsocketHandler(); export default new WebsocketHandler();

View File

@@ -33,12 +33,9 @@ interface IConfig {
ADVANCED_GBT_MEMPOOL: boolean; ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean; CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number; MAX_BLOCKS_BULK_QUERY: number;
DISK_CACHE_BLOCK_INTERVAL: number;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
RETRY_UNIX_SOCKET_AFTER: number;
}; };
LIGHTNING: { LIGHTNING: {
ENABLED: boolean; ENABLED: boolean;
@@ -54,7 +51,6 @@ interface IConfig {
TLS_CERT_PATH: string; TLS_CERT_PATH: string;
MACAROON_PATH: string; MACAROON_PATH: string;
REST_API_URL: string; REST_API_URL: string;
TIMEOUT: number;
}; };
CLIGHTNING: { CLIGHTNING: {
SOCKET: string; SOCKET: string;
@@ -69,14 +65,12 @@ interface IConfig {
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
PORT: number; PORT: number;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
DATABASE: { DATABASE: {
ENABLED: boolean; ENABLED: boolean;
@@ -86,7 +80,6 @@ interface IConfig {
DATABASE: string; DATABASE: string;
USERNAME: string; USERNAME: string;
PASSWORD: string; PASSWORD: string;
TIMEOUT: number;
}; };
SYSLOG: { SYSLOG: {
ENABLED: boolean; ENABLED: boolean;
@@ -162,12 +155,9 @@ const defaults: IConfig = {
'ADVANCED_GBT_MEMPOOL': false, 'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'MAX_BLOCKS_BULK_QUERY': 0,
'DISK_CACHE_BLOCK_INTERVAL': 6,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'RETRY_UNIX_SOCKET_AFTER': 30000,
}, },
'ELECTRUM': { 'ELECTRUM': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@@ -178,15 +168,13 @@ const defaults: IConfig = {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool'
'TIMEOUT': 60000,
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
'PORT': 8332, 'PORT': 8332,
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool'
'TIMEOUT': 60000,
}, },
'DATABASE': { 'DATABASE': {
'ENABLED': true, 'ENABLED': true,
@@ -195,8 +183,7 @@ const defaults: IConfig = {
'PORT': 3306, 'PORT': 3306,
'DATABASE': 'mempool', 'DATABASE': 'mempool',
'USERNAME': 'mempool', 'USERNAME': 'mempool',
'PASSWORD': 'mempool', 'PASSWORD': 'mempool'
'TIMEOUT': 180000,
}, },
'SYSLOG': { 'SYSLOG': {
'ENABLED': true, 'ENABLED': true,
@@ -227,7 +214,6 @@ const defaults: IConfig = {
'TLS_CERT_PATH': '', 'TLS_CERT_PATH': '',
'MACAROON_PATH': '', 'MACAROON_PATH': '',
'REST_API_URL': 'https://localhost:8080', 'REST_API_URL': 'https://localhost:8080',
'TIMEOUT': 10000,
}, },
'CLIGHTNING': { 'CLIGHTNING': {
'SOCKET': '', 'SOCKET': '',

View File

@@ -33,32 +33,8 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]> OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
{ {
this.checkDBFlag(); this.checkDBFlag();
let hardTimeout; const pool = await this.getPool();
if (query?.timeout != null) { return pool.query(query, params);
hardTimeout = Math.floor(query.timeout * 1.1);
} else {
hardTimeout = config.DATABASE.TIMEOUT;
}
if (hardTimeout > 0) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`DB query failed to return, reject or time out within ${hardTimeout / 1000}s - ${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}`));
}, hardTimeout);
this.getPool().then(pool => {
return pool.query(query, params) as Promise<[T, FieldPacket[]]>;
}).then(result => {
resolve(result);
}).catch(error => {
reject(error);
}).finally(() => {
clearTimeout(timer);
});
});
} else {
const pool = await this.getPool();
return pool.query(query, params);
}
} }
public async checkDbConnection() { public async checkDbConnection() {

View File

@@ -2,7 +2,6 @@ import express from 'express';
import { Application, Request, Response, NextFunction } from 'express'; import { Application, Request, Response, NextFunction } from 'express';
import * as http from 'http'; import * as http from 'http';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import cluster from 'cluster'; import cluster from 'cluster';
import DB from './database'; import DB from './database';
import config from './config'; import config from './config';
@@ -46,8 +45,7 @@ class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
private server: http.Server | undefined; private server: http.Server | undefined;
private app: Application; private app: Application;
private currentBackendRetryInterval = 1; private currentBackendRetryInterval = 5;
private backendRetryCount = 0;
private maxHeapSize: number = 0; private maxHeapSize: number = 0;
private heapLogInterval: number = 60; private heapLogInterval: number = 60;
@@ -122,7 +120,7 @@ class Server {
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$();
if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.ENABLED) {
await diskCache.$loadMempoolCache(); diskCache.loadMempoolCache();
} }
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
@@ -180,26 +178,22 @@ class Server {
logger.debug(msg); logger.debug(msg);
} }
} }
const newMempool = await bitcoinApi.$getRawMempool(); await blocks.$updateBlocks();
const numHandledBlocks = await blocks.$updateBlocks(); await memPool.$updateMempool();
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool);
}
indexer.$run(); indexer.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 1 : config.MEMPOOL.POLL_RATE_MS); this.currentBackendRetryInterval = 5;
this.backendRetryCount = 0;
} catch (e: any) { } catch (e: any) {
this.backendRetryCount++; let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`;
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`; loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
if (e?.stack) { if (e?.stack) {
loggerMsg += ` Stack trace: ${e.stack}`; loggerMsg += ` Stack trace: ${e.stack}`;
} }
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds // When we get a first Exception, only `logger.debug` it and retry after 5 seconds
// From the second Exception, `logger.warn` the Exception and increase the retry delay // From the second Exception, `logger.warn` the Exception and increase the retry delay
if (this.backendRetryCount >= 5) { // Maximum retry delay is 60 seconds
if (this.currentBackendRetryInterval > 5) {
logger.warn(loggerMsg); logger.warn(loggerMsg);
mempool.setOutOfSync(); mempool.setOutOfSync();
} else { } else {
@@ -209,8 +203,8 @@ class Server {
logger.debug(`AxiosError: ${e?.message}`); logger.debug(`AxiosError: ${e?.message}`);
} }
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval); setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
} finally { this.currentBackendRetryInterval *= 2;
diskCache.unlock(); this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
} }
} }
@@ -221,11 +215,11 @@ class Server {
await lightningStatsUpdater.$startService(); await lightningStatsUpdater.$startService();
await forensicsService.$startService(); await forensicsService.$startService();
} catch(e) { } catch(e) {
logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60); await Common.sleep$(1000 * 60);
this.$runLightningBackend(); this.$runLightningBackend();
}; };
} }
setUpWebsocketHandling(): void { setUpWebsocketHandling(): void {
if (this.wss) { if (this.wss) {
@@ -243,7 +237,7 @@ class Server {
websocketHandler.setupConnectionHandling(); websocketHandler.setupConnectionHandling();
if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.ENABLED) {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler)); statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler)); memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler)); blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
} }
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
@@ -281,7 +275,7 @@ class Server {
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) { if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true; this.warnedHeapCritical = true;
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit * 100).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`); logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
} }
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) { if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`); logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);

View File

@@ -69,10 +69,6 @@ class Logger {
this.network = this.getNetwork(); this.network = this.getNetwork();
} }
public updateNetwork(): void {
this.network = this.getNetwork();
}
private addprio(prio): void { private addprio(prio): void {
this[prio] = (function(_this) { this[prio] = (function(_this) {
return function(msg, tag?: string) { return function(msg, tag?: string) {

View File

@@ -58,7 +58,6 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
export interface MempoolBlockDelta { export interface MempoolBlockDelta {
added: TransactionStripped[]; added: TransactionStripped[];
removed: string[]; removed: string[];
changed: { txid: string, rate: number | undefined }[];
} }
interface VinStrippedToScriptsig { interface VinStrippedToScriptsig {
@@ -80,22 +79,18 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
descendants?: Ancestor[]; descendants?: Ancestor[];
bestDescendant?: BestDescendant | null; bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean; cpfpChecked?: boolean;
position?: { deleteAfter?: number;
block: number,
vsize: number,
};
uid?: number;
} }
export interface AuditTransaction { export interface AuditTransaction {
uid: number; txid: string;
fee: number; fee: number;
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize: number; effectiveFeePerVsize: number;
inputs: number[]; vin: string[];
relativesSet: boolean; relativesSet: boolean;
ancestorMap: Map<number, AuditTransaction>; ancestorMap: Map<string, AuditTransaction>;
children: Set<AuditTransaction>; children: Set<AuditTransaction>;
ancestorFee: number; ancestorFee: number;
ancestorWeight: number; ancestorWeight: number;
@@ -105,25 +100,13 @@ export interface AuditTransaction {
modifiedNode: HeapNode<AuditTransaction>; modifiedNode: HeapNode<AuditTransaction>;
} }
export interface CompactThreadTransaction {
uid: number;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
cpfpRoot?: string;
cpfpChecked?: boolean;
dirty?: boolean;
}
export interface ThreadTransaction { export interface ThreadTransaction {
txid: string; txid: string;
fee: number; fee: number;
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
inputs: number[]; vin: string[];
cpfpRoot?: string; cpfpRoot?: string;
cpfpChecked?: boolean; cpfpChecked?: boolean;
} }
@@ -162,7 +145,6 @@ export interface TransactionStripped {
fee: number; fee: number;
vsize: number; vsize: number;
value: number; value: number;
rate?: number; // effective fee rate
} }
export interface BlockExtension { export interface BlockExtension {
@@ -232,16 +214,6 @@ export interface MempoolStats {
tx_count: number; tx_count: number;
} }
export interface EffectiveFeeStats {
medianFee: number; // median effective fee rate
feeRange: number[]; // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles
}
export interface CpfpSummary {
transactions: TransactionExtended[];
clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[];
}
export interface Statistic { export interface Statistic {
id?: number; id?: number;
added: string; added: string;
@@ -337,11 +309,9 @@ export interface IDifficultyAdjustment {
remainingBlocks: number; remainingBlocks: number;
remainingTime: number; remainingTime: number;
previousRetarget: number; previousRetarget: number;
previousTime: number;
nextRetargetHeight: number; nextRetargetHeight: number;
timeAvg: number; timeAvg: number;
timeOffset: number; timeOffset: number;
expectedBlocks: number;
} }
export interface IndexedDifficultyAdjustment { export interface IndexedDifficultyAdjustment {

View File

@@ -1,4 +1,4 @@
import { BlockExtended, BlockExtension, BlockPrice, EffectiveFeeStats } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockPrice } from '../mempool.interfaces';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { Common } from '../api/common'; import { Common } from '../api/common';
@@ -13,48 +13,6 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks'; import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository'; import BlocksAuditsRepository from './BlocksAuditsRepository';
interface DatabaseBlock {
id: string;
height: number;
version: number;
timestamp: number;
bits: number;
nonce: number;
difficulty: number;
merkle_root: string;
tx_count: number;
size: number;
weight: number;
previousblockhash: string;
mediantime: number;
totalFees: number;
medianFee: number;
feeRange: string;
reward: number;
poolId: number;
poolName: string;
poolSlug: string;
avgFee: number;
avgFeeRate: number;
coinbaseRaw: string;
coinbaseAddress: string;
coinbaseSignature: string;
coinbaseSignatureAscii: string;
avgTxSize: number;
totalInputs: number;
totalOutputs: number;
totalOutputAmt: number;
medianFeeAmt: number;
feePercentiles: string;
segwitTotalTxs: number;
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
}
const BLOCK_DB_FIELDS = ` const BLOCK_DB_FIELDS = `
blocks.hash AS id, blocks.hash AS id,
blocks.height, blocks.height,
@@ -94,7 +52,7 @@ const BLOCK_DB_FIELDS = `
blocks.header, blocks.header,
blocks.utxoset_change AS utxoSetChange, blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize, blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt blocks.total_input_amt AS totalInputAmts
`; `;
class BlocksRepository { class BlocksRepository {
@@ -213,32 +171,6 @@ class BlocksRepository {
} }
} }
/**
* Update missing fee amounts fields
*
* @param blockHash
* @param feeAmtPercentiles
* @param medianFeeAmt
*/
public async $updateFeeAmounts(blockHash: string, feeAmtPercentiles, medianFeeAmt) : Promise<void> {
try {
const query = `
UPDATE blocks
SET fee_percentiles = ?, median_fee_amt = ?
WHERE hash = ?
`;
const params: any[] = [
JSON.stringify(feeAmtPercentiles),
medianFeeAmt,
blockHash
];
await DB.query(query, params);
} catch (e: any) {
logger.err(`Cannot update fee amounts for block ${blockHash}. Reason: ' + ${e instanceof Error ? e.message : e}`);
throw e;
}
}
/** /**
* Get all block height that have not been indexed between [startHeight, endHeight] * Get all block height that have not been indexed between [startHeight, endHeight]
*/ */
@@ -500,7 +432,7 @@ class BlocksRepository {
const blocks: BlockExtended[] = []; const blocks: BlockExtended[] = [];
for (const block of rows) { for (const block of rows) {
blocks.push(await this.formatDbBlockIntoExtendedBlock(block as DatabaseBlock)); blocks.push(await this.formatDbBlockIntoExtendedBlock(block));
} }
return blocks; return blocks;
@@ -527,13 +459,37 @@ class BlocksRepository {
return null; return null;
} }
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock); return await this.formatDbBlockIntoExtendedBlock(rows[0]);
} catch (e) { } catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT ${BLOCK_DB_FIELDS}
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = ?;
`;
const [rows]: any[] = await DB.query(query, [hash]);
if (rows.length <= 0) {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Return blocks difficulty * Return blocks difficulty
*/ */
@@ -643,6 +599,7 @@ class BlocksRepository {
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) { if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`); logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
await this.$deleteBlocksFrom(blocks[idx - 1].height); await this.$deleteBlocksFrom(blocks[idx - 1].height);
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800); await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height); await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
return false; return false;
@@ -662,7 +619,7 @@ class BlocksRepository {
* Delete blocks from the database from blockHeight * Delete blocks from the database from blockHeight
*/ */
public async $deleteBlocksFrom(blockHeight: number) { public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks from height ${blockHeight} from the database`, logger.tags.mining); logger.info(`Delete newer blocks from height ${blockHeight} from the database`);
try { try {
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`); await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
@@ -951,32 +908,13 @@ class BlocksRepository {
} }
} }
/**
* Save indexed effective fee statistics
*
* @param id
* @param feeStats
*/
public async $saveEffectiveFeeStats(id: string, feeStats: EffectiveFeeStats): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET median_fee = ?, fee_span = ?
WHERE hash = ?`,
[feeStats.medianFee, JSON.stringify(feeStats.feeRange), id]
);
} catch (e) {
logger.err(`Cannot update block fee stats. 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
* *
* @param dbBlk * @param dbBlk
*/ */
private async formatDbBlockIntoExtendedBlock(dbBlk: DatabaseBlock): Promise<BlockExtended> { private async formatDbBlockIntoExtendedBlock(dbBlk: any): Promise<BlockExtended> {
const blk: Partial<BlockExtended> = {}; const blk: Partial<BlockExtended> = {};
const extras: Partial<BlockExtension> = {}; const extras: Partial<BlockExtension> = {};
@@ -1040,7 +978,6 @@ class BlocksRepository {
} }
// If we're missing block summary related field, check if we can populate them on the fly now // If we're missing block summary related field, check if we can populate them on the fly now
// This is for example triggered upon re-org
if (Common.blocksSummariesIndexingEnabled() && if (Common.blocksSummariesIndexingEnabled() &&
(extras.medianFeeAmt === null || extras.feePercentiles === null)) (extras.medianFeeAmt === null || extras.feePercentiles === null))
{ {
@@ -1048,12 +985,11 @@ class BlocksRepository {
if (extras.feePercentiles === null) { if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2); const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block); const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions); await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id); extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
} }
if (extras.feePercentiles !== null) { if (extras.feePercentiles !== null) {
extras.medianFeeAmt = extras.feePercentiles[3]; extras.medianFeeAmt = extras.feePercentiles[3];
await this.$updateFeeAmounts(dbBlk.id, extras.feePercentiles, extras.medianFeeAmt);
} }
} }

View File

@@ -1,6 +1,6 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockSummary, TransactionStripped } from '../mempool.interfaces'; import { BlockSummary } from '../mempool.interfaces';
class BlocksSummariesRepository { class BlocksSummariesRepository {
public async $getByBlockId(id: string): Promise<BlockSummary | undefined> { public async $getByBlockId(id: string): Promise<BlockSummary | undefined> {
@@ -17,17 +17,23 @@ class BlocksSummariesRepository {
return undefined; return undefined;
} }
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> { public async $saveSummary(params: { height: number, mined?: BlockSummary}) {
const blockId = params.mined?.id;
try { try {
const transactionsStr = JSON.stringify(transactions); const transactions = JSON.stringify(params.mined?.transactions || []);
await DB.query(` await DB.query(`
INSERT INTO blocks_summaries INSERT INTO blocks_summaries (height, id, transactions, template)
SET height = ?, transactions = ?, id = ? VALUE (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE transactions = ?`, ON DUPLICATE KEY UPDATE
[blockHeight, transactionsStr, blockId, transactionsStr]); transactions = ?
`, [params.height, blockId, transactions, '[]', transactions]);
} catch (e: any) { } catch (e: any) {
logger.debug(`Cannot save block summary transactions for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`); if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
throw e; logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
} else {
logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
} }
} }
@@ -62,6 +68,19 @@ class BlocksSummariesRepository {
return []; return [];
} }
/**
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks_summaries where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/** /**
* 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

@@ -48,7 +48,7 @@ class CpfpRepository {
} }
} }
public async $batchSaveClusters(clusters: { root: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number }[]): Promise<boolean> { public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise<boolean> {
try { try {
const clusterValues: any[] = []; const clusterValues: any[] = [];
const txs: any[] = []; const txs: any[] = [];

View File

@@ -220,7 +220,7 @@ class HashratesRepository {
* Delete hashrates from the database from timestamp * Delete hashrates from the database from timestamp
*/ */
public async $deleteHashratesFromTimestamp(timestamp: number) { public async $deleteHashratesFromTimestamp(timestamp: number) {
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`, logger.tags.mining); logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`);
try { try {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]); await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);

View File

@@ -160,7 +160,7 @@ class PricesRepository {
// Compute fiat exchange rates // Compute fiat exchange rates
let latestPrice = rates[0] as ApiPrice; let latestPrice = rates[0] as ApiPrice;
if (!latestPrice || latestPrice.USD === -1) { if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj(); latestPrice = priceUpdater.getEmptyPricesObj();
} }

View File

@@ -27,7 +27,7 @@ class ForensicsService {
private async $runTasks(): Promise<void> { private async $runTasks(): Promise<void> {
try { try {
logger.debug(`Running forensics scans`); logger.info(`Running forensics scans`);
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
await this.$runClosedChannelsForensics(false); await this.$runClosedChannelsForensics(false);
@@ -73,7 +73,7 @@ class ForensicsService {
let progress = 0; let progress = 0;
try { try {
logger.debug(`Started running closed channel forensics...`); logger.info(`Started running closed channel forensics...`);
let channels; let channels;
if (onlyNewChannels) { if (onlyNewChannels) {
channels = await channelsApi.$getClosedChannelsWithoutReason(); channels = await channelsApi.$getClosedChannelsWithoutReason();
@@ -152,11 +152,11 @@ class ForensicsService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
logger.debug(`Updating channel closed channel forensics ${progress}/${channels.length}`); logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }
logger.debug(`Closed channels forensics scan complete.`); logger.info(`Closed channels forensics scan complete.`);
} catch (e) { } catch (e) {
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
} }
@@ -217,7 +217,7 @@ class ForensicsService {
let progress = 0; let progress = 0;
try { try {
logger.debug(`Started running open channel forensics...`); logger.info(`Started running open channel forensics...`);
const channels = await channelsApi.$getChannelsWithoutSourceChecked(); const channels = await channelsApi.$getChannelsWithoutSourceChecked();
for (const openChannel of channels) { for (const openChannel of channels) {
@@ -257,7 +257,7 @@ class ForensicsService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > 10) { if (elapsedSeconds > 10) {
logger.debug(`Updating opened channel forensics ${progress}/${channels?.length}`); logger.info(`Updating opened channel forensics ${progress}/${channels?.length}`);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
this.truncateTempCache(); this.truncateTempCache();
} }
@@ -266,7 +266,7 @@ class ForensicsService {
} }
} }
logger.debug(`Open channels forensics scan complete.`); logger.info(`Open channels forensics scan complete.`);
} catch (e) { } catch (e) {
logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e)); logger.err('$runOpenedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
} finally { } finally {

View File

@@ -283,7 +283,7 @@ class NetworkSyncService {
} else { } else {
log += ` for the first time`; log += ` for the first time`;
} }
logger.debug(`${log}`, logger.tags.ln); logger.info(`${log}`, logger.tags.ln);
const channels = await channelsApi.$getChannelsByStatus([0, 1]); const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) { for (const channel of channels) {
@@ -300,7 +300,7 @@ class NetworkSyncService {
++progress; ++progress;
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
logger.debug(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln); logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }

View File

@@ -22,15 +22,12 @@ class LightningStatsUpdater {
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds * Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
*/ */
private async $logStatsDaily(): Promise<void> { private async $logStatsDaily(): Promise<void> {
try { const date = new Date();
const date = new Date(); Common.setDateMidnight(date);
Common.setDateMidnight(date); const networkGraph = await lightningApi.$getNetworkGraph();
const networkGraph = await lightningApi.$getNetworkGraph(); await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln); logger.debug(`Updated latest network stats`, logger.tags.ln);
} catch (e) {
logger.err(`Exception in $logStatsDaily. Reason: ${(e instanceof Error ? e.message : e)}`);
}
} }
} }

View File

@@ -15,20 +15,16 @@ class LightningStatsImporter {
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
async $run(): Promise<void> { async $run(): Promise<void> {
try { const [channels]: any[] = await DB.query('SELECT short_id from channels;');
const [channels]: any[] = await DB.query('SELECT short_id from channels;'); logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln);
logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln); await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) { if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
return; return;
}
await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot();
} catch (e) {
logger.err(`Exception in LightningStatsImporter::$run(). ${e}`);
} }
await this.$importHistoricalLightningStats();
await this.$cleanupIncorrectSnapshot();
} }
/** /**

View File

@@ -62,7 +62,7 @@ class PoolsUpdater {
if (this.currentSha === null) { if (this.currentSha === null) {
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} else { } else {
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
} }
const poolsJson = await this.query(this.poolsUrl); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {

View File

@@ -222,7 +222,7 @@ class PriceUpdater {
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> { private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
const existingPriceTimes = await PricesRepository.$getPricesTimes(); const existingPriceTimes = await PricesRepository.$getPricesTimes();
logger.debug(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining); logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining);
const historicalPrices: PriceHistory[] = []; const historicalPrices: PriceHistory[] = [];

View File

@@ -5,7 +5,6 @@
"types": ["node", "jest"], "types": ["node", "jest"],
"lib": ["es2019", "dom"], "lib": ["es2019", "dom"],
"strict": true, "strict": true,
"skipLibCheck": true,
"noImplicitAny": false, "noImplicitAny": false,
"sourceMap": false, "sourceMap": false,
"outDir": "dist", "outDir": "dist",

View File

@@ -34,7 +34,6 @@ If you want to use different credentials, specify them in the `docker-compose.ym
CORE_RPC_PORT: "8332" CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "customuser" CORE_RPC_USERNAME: "customuser"
CORE_RPC_PASSWORD: "custompassword" CORE_RPC_PASSWORD: "custompassword"
CORE_RPC_TIMEOUT: "60000"
``` ```
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly. The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
@@ -113,7 +112,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0, "MAX_BLOCKS_BULK_QUERY": 0,
"DISK_CACHE_BLOCK_INTERVAL": 6
}, },
``` ```
@@ -145,7 +143,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_MEMPOOL: "" MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: "" MAX_BLOCKS_BULK_QUERY: ""
DISK_CACHE_BLOCK_INTERVAL: ""
... ...
``` ```
@@ -161,8 +158,7 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool"
"TIMEOUT": 60000
}, },
``` ```
@@ -174,7 +170,6 @@ Corresponding `docker-compose.yml` overrides:
CORE_RPC_PORT: "" CORE_RPC_PORT: ""
CORE_RPC_USERNAME: "" CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: "" CORE_RPC_PASSWORD: ""
CORE_RPC_TIMEOUT: 60000
... ...
``` ```
@@ -204,9 +199,7 @@ Corresponding `docker-compose.yml` overrides:
`mempool-config.json`: `mempool-config.json`:
```json ```json
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000", "REST_API_URL": "http://127.0.0.1:3000"
"UNIX_SOCKET_PATH": "/tmp/esplora-socket",
"RETRY_UNIX_SOCKET_AFTER": 30000
}, },
``` ```
@@ -215,8 +208,6 @@ Corresponding `docker-compose.yml` overrides:
api: api:
environment: environment:
ESPLORA_REST_API_URL: "" ESPLORA_REST_API_URL: ""
ESPLORA_UNIX_SOCKET_PATH: ""
ESPLORA_RETRY_UNIX_SOCKET_AFTER: ""
... ...
``` ```
@@ -228,8 +219,7 @@ Corresponding `docker-compose.yml` overrides:
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
"PASSWORD": "mempool", "PASSWORD": "mempool"
"TIMEOUT": 60000
}, },
``` ```
@@ -241,7 +231,6 @@ Corresponding `docker-compose.yml` overrides:
SECOND_CORE_RPC_PORT: "" SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: "" SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: "" SECOND_CORE_RPC_PASSWORD: ""
SECOND_CORE_RPC_TIMEOUT: ""
... ...
``` ```
@@ -269,7 +258,6 @@ Corresponding `docker-compose.yml` overrides:
DATABASE_DATABASE: "" DATABASE_DATABASE: ""
DATABASE_USERNAME: "" DATABASE_USERNAME: ""
DATABASE_PASSWORD: "" DATABASE_PASSWORD: ""
DATABASE_TIMEOUT: ""
... ...
``` ```
@@ -415,7 +403,6 @@ Corresponding `docker-compose.yml` overrides:
"TLS_CERT_PATH": "" "TLS_CERT_PATH": ""
"MACAROON_PATH": "" "MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080" "REST_API_URL": "https://localhost:8080"
"TIMEOUT": 10000
} }
``` ```
@@ -426,7 +413,6 @@ Corresponding `docker-compose.yml` overrides:
LND_TLS_CERT_PATH: "" LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: "" LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080" LND_REST_API_URL: "https://localhost:8080"
LND_TIMEOUT: 10000
... ...
``` ```
@@ -464,8 +450,8 @@ Corresponding `docker-compose.yml` overrides:
api: api:
environment: environment:
MAXMIND_ENABLED: true, MAXMIND_ENABLED: true,
MAXMIND_GEOLITE2_CITY: "/backend/GeoIP/GeoLite2-City.mmdb", MAXMIND_GEOLITE2_CITY: "./backend/GeoIP/GeoLite2-City.mmdb",
MAXMIND_GEOLITE2_ASN": "/backend/GeoIP/GeoLite2-ASN.mmdb", MAXMIND_GEOLITE2_ASN": "./backend/GeoIP/GeoLite2-ASN.mmdb",
MAXMIND_GEOIP2_ISP": "/backend/GeoIP/GeoIP2-ISP.mmdb" MAXMIND_GEOIP2_ISP": "./backend/GeoIP/GeoIP2-ISP.mmdb"
... ...
``` ```

View File

@@ -17,7 +17,6 @@ WORKDIR /backend
RUN chown 1000:1000 ./ RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/ COPY --from=builder --chown=1000:1000 /build/package ./package/
COPY --from=builder --chown=1000:1000 /build/GeoIP ./GeoIP/
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./ COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
USER 1000 USER 1000

View File

@@ -26,15 +26,13 @@
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__, "ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__, "ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__, "CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__, "MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__
"DISK_CACHE_BLOCK_INTERVAL": __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
"PORT": __CORE_RPC_PORT__, "PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__", "USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__"
"TIMEOUT": __CORE_RPC_TIMEOUT__
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",
@@ -42,16 +40,13 @@
"TLS_ENABLED": __ELECTRUM_TLS_ENABLED__ "TLS_ENABLED": __ELECTRUM_TLS_ENABLED__
}, },
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__"
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
}, },
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__", "HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": __SECOND_CORE_RPC_PORT__, "PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__", "USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__", "PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
"TIMEOUT": __SECOND_CORE_RPC_TIMEOUT__
}, },
"DATABASE": { "DATABASE": {
"ENABLED": __DATABASE_ENABLED__, "ENABLED": __DATABASE_ENABLED__,
@@ -60,8 +55,7 @@
"PORT": __DATABASE_PORT__, "PORT": __DATABASE_PORT__,
"DATABASE": "__DATABASE_DATABASE__", "DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__", "USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__", "PASSWORD": "__DATABASE_PASSWORD__"
"TIMEOUT": "__DATABASE_TIMEOUT__"
}, },
"SYSLOG": { "SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__, "ENABLED": __SYSLOG_ENABLED__,
@@ -89,8 +83,7 @@
"LND": { "LND": {
"TLS_CERT_PATH": "__LND_TLS_CERT_PATH__", "TLS_CERT_PATH": "__LND_TLS_CERT_PATH__",
"MACAROON_PATH": "__LND_MACAROON_PATH__", "MACAROON_PATH": "__LND_MACAROON_PATH__",
"REST_API_URL": "__LND_REST_API_URL__", "REST_API_URL": "__LND_REST_API_URL__"
"TIMEOUT": "__LND_TIMEOUT__"
}, },
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__" "SOCKET": "__CLIGHTNING_SOCKET__"
@@ -116,7 +109,7 @@
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__" "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
}, },
"MAXMIND": { "MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__, "ENABLED": "__MAXMIND_ENABLED__",
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__", "GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__", "GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__" "GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"

View File

@@ -30,14 +30,12 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false} __MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
__CORE_RPC_PORT__=${CORE_RPC_PORT:=8332} __CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool} __CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@@ -46,15 +44,12 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=null}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
# 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}
__SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332} __SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool} __SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool} __SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
__SECOND_CORE_RPC_TIMEOUT__=${SECOND_CORE_RPC_TIMEOUT:=60000}
# DATABASE # DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true} __DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
@@ -64,7 +59,6 @@ __DATABASE_PORT__=${DATABASE_PORT:=3306}
__DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool} __DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool}
__DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool} __DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool} __DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
__DATABASE_TIMEOUT__=${DATABASE_TIMEOUT:=180000}
# SYSLOG # SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false} __SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
@@ -113,15 +107,14 @@ __LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""} __LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""} __LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"} __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
__LND_TIMEOUT__=${LND_TIMEOUT:=10000}
# CLN # CLN
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""} __CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
# MAXMIND # MAXMIND
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true} __MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City.mmdb"} __MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="./backend/GeoIP/GeoLite2-City.mmdb"}
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"} __MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="./backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""} __MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
@@ -156,27 +149,22 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json sed -i "s/__CORE_RPC_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__CORE_RPC_TIMEOUT__/${__CORE_RPC_TIMEOUT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s/__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
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_TIMEOUT__/${__SECOND_CORE_RPC_TIMEOUT__}/g" mempool-config.json
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
@@ -228,7 +216,6 @@ sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTER
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
sed -i "s!__LND_TIMEOUT__!${__LND_TIMEOUT__}!g" mempool-config.json
# CLN # CLN
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json

View File

@@ -10,10 +10,6 @@ cp /etc/nginx/nginx.conf /patch/nginx.conf
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
cat /patch/nginx.conf > /etc/nginx/nginx.conf cat /patch/nginx.conf > /etc/nginx/nginx.conf
if [ "${LIGHTNING_DETECTED_PORT}" != "" ];then
export LIGHTNING=true
fi
# Runtime overrides - read env vars defined in docker compose # Runtime overrides - read env vars defined in docker compose
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false} __TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
@@ -39,7 +35,6 @@ __AUDIT__=${AUDIT:=false}
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0} __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}
__FULL_RBF_ENABLED__=${FULL_RBF_ENABLED:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
@@ -66,7 +61,6 @@ export __AUDIT__
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__ 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 __FULL_RBF_ENABLED__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)

View File

@@ -4,7 +4,6 @@
cp ./docker/backend/* ./backend/ cp ./docker/backend/* ./backend/
#geoip-data #geoip-data
mkdir -p ./backend/GeoIP/
wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb
wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb

View File

@@ -106,7 +106,6 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Arabic @baro0k * Arabic @baro0k
* Czech @pixelmade2 * Czech @pixelmade2
* Danish @pierrevendelboe
* German @Emzy * German @Emzy
* English (default) * English (default)
* Spanish @maxhodler @bisqes * Spanish @maxhodler @bisqes
@@ -114,7 +113,6 @@ https://www.transifex.com/mempool/mempool/dashboard/
* French @Bayernatoor * French @Bayernatoor
* Korean @kcalvinalvinn @sogoagain * Korean @kcalvinalvinn @sogoagain
* Italian @HodlBits * Italian @HodlBits
* Lithuanian @eimze21
* Hebrew @rapidlab309 * Hebrew @rapidlab309
* Georgian @wyd_idk * Georgian @wyd_idk
* Hungarian @btcdragonlord * Hungarian @btcdragonlord

View File

@@ -158,10 +158,10 @@ describe('Liquid', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -169,8 +169,8 @@ describe('Liquid', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`); cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -109,10 +109,10 @@ describe('Liquid Testnet', () => {
it('show empty unblinded TX', () => { it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`); cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential'); cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
}); });
@@ -120,8 +120,8 @@ describe('Liquid Testnet', () => {
it('show invalid unblinded TX hex', () => { it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`); cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', 'ng-star-inserted'); cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)'); cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
}); });

View File

@@ -127,7 +127,7 @@ describe('Mainnet', () => {
cy.get('.search-box-container > .form-control').type('S').then(() => { cy.get('.search-box-container > .form-control').type('S').then(() => {
cy.wait('@search-1wizS'); cy.wait('@search-1wizS');
cy.get('app-search-results button.dropdown-item').should('have.length', 6); cy.get('app-search-results button.dropdown-item').should('have.length', 5);
}); });
cy.get('.search-box-container > .form-control').type('A').then(() => { cy.get('.search-box-container > .form-control').type('A').then(() => {

View File

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

View File

@@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "2.5.0-dev",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "2.5.0-dev",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "^14.2.10", "@angular-devkit/build-angular": "^14.2.10",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.6.0-dev", "version": "2.5.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",

View File

@@ -4,8 +4,6 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component'; import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component'; import { BlockComponent } from './components/block/block.component';
import { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
import { AddressComponent } from './components/address/address.component'; import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
@@ -16,7 +14,6 @@ import { TrademarkPolicyComponent } from './components/trademark-policy/trademar
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component'; import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { BlocksList } from './components/blocks-list/blocks-list.component'; import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component'; import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component'; import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
@@ -59,10 +56,6 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -169,10 +162,6 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -275,10 +264,6 @@ let routes: Routes = [
path: 'blocks', path: 'blocks',
component: BlocksList, component: BlocksList,
}, },
{
path: 'rbf',
component: RbfList,
},
{ {
path: 'terms-of-service', path: 'terms-of-service',
component: TermsOfServiceComponent component: TermsOfServiceComponent
@@ -357,14 +342,6 @@ let routes: Routes = [
}, },
], ],
}, },
{
path: 'clock-mined',
component: ClockMinedComponent,
},
{
path: 'clock-mempool',
component: ClockMempoolComponent,
},
{ {
path: 'status', path: 'status',
data: { networks: ['bitcoin', 'liquid'] }, data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -29,14 +29,6 @@ export const mempoolFeeColors = [
'ba3243', 'ba3243',
'b92b48', 'b92b48',
'b9254b', 'b9254b',
'b8214d',
'b71d4f',
'b61951',
'b41453',
'b30e55',
'b10857',
'b00259',
'ae005b',
]; ];
export const chartColors = [ export const chartColors = [
@@ -77,7 +69,6 @@ export const chartColors = [
"#3E2723", "#3E2723",
"#212121", "#212121",
"#263238", "#263238",
"#801313",
]; ];
export const poolsColor = { export const poolsColor = {

View File

@@ -107,7 +107,12 @@
<app-language-selector></app-language-selector> <app-language-selector></app-language-selector>
<app-global-footer></app-global-footer> <div class="text-small text-center mt-3">
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
|
<a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a>
</div>
</div> </div>
<ng-template #loadingTmpl> <ng-template #loadingTmpl>

View File

@@ -107,7 +107,22 @@
<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 xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span> <span>Unchained</span>
</a> </a>
<a href="https://gemini.com/" target="_blank" title="Gemini"> <a href="https://gemini.com/" target="_blank" title="Gemini">
@@ -186,12 +201,12 @@
<span>Umbrel</span> <span>Umbrel</span>
</a> </a>
<a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz"> <a href="https://github.com/rootzoll/raspiblitz" target="_blank" title="RaspiBlitz">
<img class="image" src="/resources/profile/raspiblitz.svg" /> <img class="image" src="/resources/profile/raspiblitz.jpg" />
<span>RaspiBlitz</span> <span>RaspiBlitz</span>
</a> </a>
<a href="https://github.com/mynodebtc/mynode" target="_blank" title="myNode"> <a href="https://github.com/mynodebtc/mynode" target="_blank" title="MyNode">
<img class="image" src="/resources/profile/mynodebtc.png" /> <img class="image" src="/resources/profile/mynodebtc.jpg" />
<span>myNode</span> <span>MyNode</span>
</a> </a>
<a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo"> <a href="https://github.com/RoninDojo/RoninDojo" target="_blank" title="RoninDojo">
<img class="image" src="/resources/profile/ronindojo.png" /> <img class="image" src="/resources/profile/ronindojo.png" />
@@ -238,7 +253,7 @@
<span>Sparrow</span> <span>Sparrow</span>
</a> </a>
<a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ"> <a href="https://github.com/ACINQ/phoenix" target="_blank" title="Phoenix Wallet by ACINQ">
<img class="image not-rounded" src="/resources/profile/phoenix.svg" /> <img class="image" src="/resources/profile/phoenix.jpg" />
<span>Phoenix</span> <span>Phoenix</span>
</a> </a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
@@ -393,6 +408,7 @@
<div class="footer-links"> <div class="footer-links">
<a href="/3rdpartylicenses.txt">Third-party Licenses</a> <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
<a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a>
<div class="social-icons"> <div class="social-icons">
<a target="_blank" href="https://github.com/mempool/mempool"> <a target="_blank" href="https://github.com/mempool/mempool">
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg> <svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="github" class="svg-inline--fa fa-github fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>
@@ -416,12 +432,9 @@
{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>] {{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]
</div> </div>
<br>
</div> </div>
<ng-template #loadingSponsors> <ng-template #loadingSponsors>
<br> <br>
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</ng-template> </ng-template>

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,19 +18,17 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex; width: 100%;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,20 +18,18 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex;
flex: 1;
width: 100%; width: 100%;
height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) { @media (max-width: 992px) {

View File

@@ -23,8 +23,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() unavailable: boolean = false; @Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false; @Input() auditHighlighting: boolean = false;
@Input() blockConversion: Price; @Input() blockConversion: Price;
@Input() pixelAlign: boolean = false; @Output() txClickEvent = new EventEmitter<TransactionStripped>();
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter<string>(); @Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter(); @Output() readyEvent = new EventEmitter();
@@ -133,9 +132,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) { if (this.scene) {
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, direction, resetLayout);
this.start(); this.start();
} }
} }
@@ -202,8 +201,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.start(); this.start();
} else { } else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting });
highlighting: this.auditHighlighting, pixelAlign: this.pixelAlign });
this.start(); this.start();
} }
} }
@@ -328,9 +326,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') { if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
this.setPreviewTx(event.offsetX, event.offsetY, true); this.setPreviewTx(event.offsetX, event.offsetY, true);
} else if (event.target === this.canvas.nativeElement) { } else if (event.target === this.canvas.nativeElement) {
const keyMod = event.shiftKey || event.ctrlKey || event.metaKey; this.onTxClick(event.offsetX, event.offsetY);
const middleClick = event.which === 2 || event.button === 1;
this.onTxClick(event.offsetX, event.offsetY, keyMod || middleClick);
} }
} }
@@ -413,12 +409,12 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} }
} }
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) { onTxClick(cssX: number, cssY: number) {
const x = cssX * window.devicePixelRatio; const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio; const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y }); const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) { if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier }); this.txClickEvent.emit(selected);
} }
} }

View File

@@ -15,7 +15,6 @@ export default class BlockScene {
gridWidth: number; gridWidth: number;
gridHeight: number; gridHeight: number;
gridSize: number; gridSize: number;
pixelAlign: boolean;
vbytesPerUnit: number; vbytesPerUnit: number;
unitPadding: number; unitPadding: number;
unitWidth: number; unitWidth: number;
@@ -24,24 +23,19 @@ export default class BlockScene {
animateUntil = 0; animateUntil = 0;
dirty: boolean; dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) { ) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }); this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
} }
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.gridSize = this.width / this.gridWidth; this.gridSize = this.width / this.gridWidth;
if (this.pixelAlign) { this.unitPadding = width / 500;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 2.5)); this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.unitWidth = this.gridSize - (this.unitPadding);
} else {
this.unitPadding = width / 500;
this.unitWidth = this.gridSize - (this.unitPadding * 2);
}
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
@@ -156,7 +150,7 @@ export default class BlockScene {
this.updateAll(startTime, 200, direction); this.updateAll(startTime, 200, direction);
} }
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now(); const startTime = performance.now();
const removed = this.removeBatch(remove, startTime, direction); const removed = this.removeBatch(remove, startTime, direction);
@@ -178,15 +172,6 @@ export default class BlockScene {
this.place(tx); this.place(tx);
}); });
} else { } else {
// update effective rates
change.forEach(tx => {
if (this.txs[tx.txid]) {
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
this.txs[tx.txid].rate = tx.rate;
this.txs[tx.txid].dirty = true;
}
});
// try to insert new txs directly // try to insert new txs directly
const remaining = []; const remaining = [];
add.map(tx => new TxView(tx, this)).sort(feeRateDescending).forEach(tx => { add.map(tx => new TxView(tx, this)).sort(feeRateDescending).forEach(tx => {
@@ -215,15 +200,14 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value)); this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
} }
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting, pixelAlign }: private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, { width: number, height: number, resolution: number, blockLimit: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, pixelAlign: boolean } orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void { ): void {
this.orientation = orientation; this.orientation = orientation;
this.flip = flip; this.flip = flip;
this.vertexArray = vertexArray; this.vertexArray = vertexArray;
this.highlightingEnabled = highlighting; this.highlightingEnabled = highlighting;
this.pixelAlign = pixelAlign;
this.scene = { this.scene = {
count: 0, count: 0,
@@ -349,12 +333,7 @@ export default class BlockScene {
private gridToScreen(position: Square | void): Square { private gridToScreen(position: Square | void): Square {
if (position) { if (position) {
const slotSize = (position.s * this.gridSize); const slotSize = (position.s * this.gridSize);
let squareSize; const squareSize = slotSize - (this.unitPadding * 2);
if (this.pixelAlign) {
squareSize = slotSize - (this.unitPadding);
} else {
squareSize = slotSize - (this.unitPadding * 2);
}
// The grid is laid out notionally left-to-right, bottom-to-top, // The grid is laid out notionally left-to-right, bottom-to-top,
// so we rotate and/or flip the y axis to match the target configuration. // so we rotate and/or flip the y axis to match the target configuration.

View File

@@ -36,7 +36,6 @@ export default class TxView implements TransactionStripped {
vsize: number; vsize: number;
value: number; value: number;
feerate: number; feerate: number;
rate?: number;
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected'; status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
context?: 'projected' | 'actual'; context?: 'projected' | 'actual';
scene?: BlockScene; scene?: BlockScene;
@@ -59,8 +58,7 @@ export default class TxView implements TransactionStripped {
this.fee = tx.fee; this.fee = tx.fee;
this.vsize = tx.vsize; this.vsize = tx.vsize;
this.value = tx.value; this.value = tx.value;
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available this.feerate = tx.fee / tx.vsize;
this.rate = tx.rate;
this.status = tx.status; this.status = tx.status;
this.initialised = false; this.initialised = false;
this.vertexArray = scene.vertexArray; this.vertexArray = scene.vertexArray;
@@ -159,8 +157,7 @@ export default class TxView implements TransactionStripped {
} }
getColor(): Color { getColor(): Color {
const rate = this.fee / this.vsize; // color by simple single-tx fee rate const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
// Normal mode // Normal mode
if (!this.scene?.highlightingEnabled) { if (!this.scene?.highlightingEnabled) {

View File

@@ -28,12 +28,6 @@
{{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td> </td>
</tr> </tr>
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
<td>
{{ effectiveRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td>
</tr>
<tr> <tr>
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> <td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (vsize | vbytes: 2)"></td> <td [innerHTML]="'&lrm;' + (vsize | vbytes: 2)"></td>

View File

@@ -20,7 +20,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
value = 0; value = 0;
vsize = 1; vsize = 1;
feeRate = 0; feeRate = 0;
effectiveRate;
tooltipPosition: Position = { x: 0, y: 0 }; tooltipPosition: Position = { x: 0, y: 0 };
@@ -52,7 +51,6 @@ export class BlockOverviewTooltipComponent implements OnChanges {
this.value = tx.value || 0; this.value = tx.value || 0;
this.vsize = tx.vsize || 1; this.vsize = tx.vsize || 1;
this.feeRate = this.fee / this.vsize; this.feeRate = this.fee / this.vsize;
this.effectiveRate = tx.rate;
} }
} }
} }

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,19 +18,17 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex; width: 100%;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,19 +18,17 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex; width: 100%;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,19 +18,17 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex; width: 100%;
flex: 1;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;

View File

@@ -612,13 +612,9 @@ export class BlockComponent implements OnInit, OnDestroy {
}); });
} }
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { onTxClick(event: TransactionStripped): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
if (!event.keyModifier) { this.router.navigate([url]);
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
} }
onTxHover(txid: string): void { onTxHover(txid: string): void {

View File

@@ -1,61 +1,53 @@
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [class.minimal]="minimal" <div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr"
[style.left]="static ? (offset || 0) + 'px' : null" [style.--block-size]="blockWidth+'px'" [style.left]="static ? (offset || 0) + 'px' : null"
*ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate"> *ngIf="static || (loadingBlocks$ | async) === false; else loadingBlocksTemplate">
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn"> <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
<ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock"> <ng-container *ngIf="connected && block && !block.loading && !block.placeholder; else placeholderBlock">
<div
*ngIf="minimal && spotlight < 0 && chainTip + spotlight + 1 === block.height"
class="spotlight-bottom"
[style.left]="blockStyles[i].left"
></div>
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i" <div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}" class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
[class.offscreen]="!static && count && i >= count"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[class.blink-bg]="isSpecial(block.height)"> [class.blink-bg]="isSpecial(block.height)">
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }" <a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a> class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height"> <div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height <a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
}}</a> }}</a>
</div> </div>
<div class="block-body"> <div class="block-body">
<ng-container *ngIf="!minimal"> <div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
<div *ngIf="block?.extras; else emptyfees" [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees"> ~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> </div>
<ng-template #emptyfees>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees">
&nbsp;
</div> </div>
<ng-template #emptyfees> </ng-template>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fees"> <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span"
&nbsp; *ngIf="block?.extras?.feeRange; else emptyfeespan">
</div> {{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{
</ng-template> block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-fee-span'" class="fee-span" i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
*ngIf="block?.extras?.feeRange; else emptyfeespan"> </div>
{{ block?.extras?.feeRange?.[0] | number:feeRounding }} - {{ <ng-template #emptyfeespan>
block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container <div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span">
i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container> &nbsp;
</div> </div>
<ng-template #emptyfeespan> </ng-template>
<div [attr.data-cy]="'bitcoin-block-offset=' + offset + '-index-' + i + '-fees'" class="fee-span"> <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo"
&nbsp; class="block-size">
</div> <app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
</ng-template> </div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo" <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo"
class="block-size"> class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div>
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount> <div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
</div> <ng-container
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + 'block-size'" *ngIf="!showMiningInfo" *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
class="block-size" [innerHTML]="'&lrm;' + (block.size | bytes: 2)"></div> <ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count"> <ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
<ng-container </div>
*ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container> <div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template> <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</div>
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
</ng-container>
</div> </div>
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined"> <div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary" <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
@@ -87,11 +79,11 @@
</div> </div>
<ng-template #loadingBlocksTemplate> <ng-template #loadingBlocksTemplate>
<div class="blocks-container" [class.time-ltr]="timeLtr" [style.--block-size]="blockWidth+'px'"> <div class="blocks-container" [class.time-ltr]="timeLtr">
<div class="flashing"> <div class="flashing">
<div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn"> <div *ngFor="let block of emptyBlocks; let i = index; trackBy: trackByBlocksFn">
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" <div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}"
[ngStyle]="emptyBlockStyles[i]" [class.offscreen]="!static && count && i >= count"></div> [ngStyle]="emptyBlockStyles[i]"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
.bitcoin-block { .bitcoin-block {
width: var(--block-size); width: 125px;
height: var(--block-size); height: 125px;
} }
.blockLink { .blockLink {
@@ -22,11 +22,7 @@
.mined-block { .mined-block {
position: absolute; position: absolute;
top: 0px; top: 0px;
transition: background 2s, left 2s, transform 1s, opacity 1s; transition: background 2s, left 2s, transform 1s;
}
.mined-block.offscreen {
opacity: 0;
} }
.mined-block.placeholder-block { .mined-block.placeholder-block {
@@ -39,11 +35,9 @@
} }
.blocks-container { .blocks-container {
--block-size: 125px;
--block-offset: calc(0.32 * var(--block-size));
position: absolute; position: absolute;
top: 0px; top: 0px;
left: var(--block-offset); left: 40px;
} }
.block-body { .block-body {
@@ -83,11 +77,11 @@
.bitcoin-block::after { .bitcoin-block::after {
content: ''; content: '';
width: var(--block-size); width: 125px;
height: calc(0.192 * var(--block-size)); height: 24px;
position:absolute; position:absolute;
top: calc(-0.192 * var(--block-size)); top: -24px;
left: calc(-0.16 * var(--block-size)); left: -20px;
background-color: #232838; background-color: #232838;
transform:skew(40deg); transform:skew(40deg);
transform-origin:top; transform-origin:top;
@@ -95,11 +89,11 @@
.bitcoin-block::before { .bitcoin-block::before {
content: ''; content: '';
width: calc(0.16 * var(--block-size)); width: 20px;
height: var(--block-size); height: 125px;
position: absolute; position: absolute;
top: calc(-0.096 * var(--block-size)); top: -12px;
left: calc(-0.16 * var(--block-size)); left: -20px;
background-color: #191c27; background-color: #191c27;
transform: skewY(50deg); transform: skewY(50deg);
@@ -175,15 +169,3 @@
transform: scaleX(-1); transform: scaleX(-1);
} }
} }
.spotlight-bottom {
position: absolute;
width: calc(0.6 * var(--block-size));
height: calc(0.25 * var(--block-size));
border-left: solid calc(0.3 * var(--block-size)) transparent;
border-bottom: solid calc(0.3 * var(--block-size)) white;
border-right: solid calc(0.3 * var(--block-size)) transparent;
transform: translate(calc(0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
border-radius: 2px;
z-index: -1;
}

View File

@@ -24,9 +24,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only) @Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
@Input() loadingTip: boolean = false; @Input() loadingTip: boolean = false;
@Input() connected: boolean = true; @Input() connected: boolean = true;
@Input() minimal: boolean = false;
@Input() blockWidth: number = 125;
@Input() spotlight: number = 0;
specialBlocks = specialBlocks; specialBlocks = specialBlocks;
network = ''; network = '';
@@ -54,10 +51,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
timeLtrSubscription: Subscription; timeLtrSubscription: Subscription;
timeLtr: boolean; timeLtr: boolean;
blockOffset: number = 155;
dividerBlockOffset: number = 205;
blockPadding: number = 30;
gradientColors = { gradientColors = {
'': ['#9339f4', '#105fb0'], '': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'], bisq: ['#9339f4', '#105fb0'],
@@ -125,7 +118,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.blockStyles = []; this.blockStyles = [];
if (this.blocksFilled && block.height > this.chainTip) { if (this.blocksFilled && block.height > this.chainTip) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -this.blockOffset : -this.dividerBlockOffset))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
setTimeout(() => { setTimeout(() => {
this.blockStyles = []; this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
@@ -166,13 +159,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.blockWidth && this.blockWidth) {
this.blockPadding = 0.24 * this.blockWidth;
this.blockOffset = this.blockWidth + this.blockPadding;
this.dividerBlockOffset = this.blockOffset + (0.4 * this.blockWidth);
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
}
if (this.static) { if (this.static) {
const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1); const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
this.updateStaticBlocks(animateSlide); this.updateStaticBlocks(animateSlide);
@@ -205,14 +191,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
this.arrowVisible = true; this.arrowVisible = true;
if (newBlockFromLeft) { if (newBlockFromLeft) {
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding - this.dividerBlockOffset; this.arrowLeftPx = blockindex * 155 + 30 - 205;
setTimeout(() => { setTimeout(() => {
this.arrowTransition = '2s'; this.arrowTransition = '2s';
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding; this.arrowLeftPx = blockindex * 155 + 30;
this.cd.markForCheck(); this.cd.markForCheck();
}, 50); }, 50);
} else { } else {
this.arrowLeftPx = blockindex * this.blockOffset + this.blockPadding; this.arrowLeftPx = blockindex * 155 + 30;
if (!animate) { if (!animate) {
setTimeout(() => { setTimeout(() => {
this.arrowTransition = '2s'; this.arrowTransition = '2s';
@@ -259,7 +245,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
this.blocks = this.blocks.slice(0, this.count); this.blocks = this.blocks.slice(0, this.count);
this.blockStyles = []; this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -this.blockOffset : 0))); this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0)));
this.cd.markForCheck(); this.cd.markForCheck();
if (animateSlide) { if (animateSlide) {
// animate blocks slide right // animate blocks slide right
@@ -301,7 +287,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
} }
return { return {
left: addLeft + this.blockOffset * index + 'px', left: addLeft + 155 * index + 'px',
background: `repeating-linear-gradient( background: `repeating-linear-gradient(
#2d3348, #2d3348,
#2d3348 ${greenBackgroundHeight}%, #2d3348 ${greenBackgroundHeight}%,
@@ -323,7 +309,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const addLeft = animateEnterFrom || 0; const addLeft = animateEnterFrom || 0;
return { return {
left: addLeft + (this.blockOffset * index) + 'px', left: addLeft + (155 * index) + 'px',
background: "#2d3348", background: "#2d3348",
}; };
} }
@@ -331,7 +317,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) { getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
const addLeft = animateEnterFrom || 0; const addLeft = animateEnterFrom || 0;
return { return {
left: addLeft + (this.blockOffset * index) + 'px', left: addLeft + (155 * index) + 'px',
}; };
} }
@@ -339,7 +325,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const addLeft = animateEnterFrom || 0; const addLeft = animateEnterFrom || 0;
return { return {
left: addLeft + this.blockOffset * this.emptyBlocks.indexOf(block) + 'px', left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
background: "#2d3348", background: "#2d3348",
}; };
} }

View File

@@ -26,7 +26,7 @@
</thead> </thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td class="height text-left" [class]="widget ? 'widget' : ''"> <td class="text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a> <a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
</td> </td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
@@ -89,6 +89,7 @@
<span class="skeleton-loader" style="max-width: 75px"></span> <span class="skeleton-loader" style="max-width: 75px"></span>
</td> </td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<img width="1" height="25" style="opacity: 0">
<span class="skeleton-loader" style="max-width: 125px"></span> <span class="skeleton-loader" style="max-width: 125px"></span>
</td> </td>
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
@@ -97,7 +98,7 @@
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'"> <td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
<span class="skeleton-loader" style="max-width: 125px"></span> <span class="skeleton-loader" style="max-width: 125px"></span>
</td> </td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="auditAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<span class="skeleton-loader" style="max-width: 75px"></span> <span class="skeleton-loader" style="max-width: 75px"></span>
</td> </td>
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"> <td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">

View File

@@ -51,12 +51,7 @@ tr, td, th {
.pool.widget { .pool.widget {
width: 40%; width: 40%;
padding-left: 24px; padding-left: 24px;
@media (min-width: 768px) AND (max-width: 926px) { @media (max-width: 376px) {
padding-left: 0px;
width: 60%;
}
@media (max-width: 430px) {
padding-left: 0px;
width: 60%; width: 60%;
} }
} }
@@ -64,10 +59,6 @@ tr, td, th {
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: text-top;
padding-left: 10px; padding-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
} }
.height { .height {
@@ -78,12 +69,6 @@ tr, td, th {
@media (max-width: 576px) { @media (max-width: 576px) {
width: 10%; width: 10%;
} }
@media (min-width: 768px) AND (max-width: 926px) {
width: 30%;
}
@media (max-width: 430px) {
width: 30%;
}
} }
.height.legacy { .height.legacy {
width: 15%; width: 15%;
@@ -107,7 +92,7 @@ tr, td, th {
.mined { .mined {
width: 13%; width: 13%;
@media (max-width: 730px) { @media (max-width: 576px) {
display: none; display: none;
} }
} }
@@ -153,7 +138,7 @@ tr, td, th {
.fees { .fees {
width: 8%; width: 8%;
@media (max-width: 820px) { @media (max-width: 650px) {
display: none; display: none;
} }
} }
@@ -178,16 +163,6 @@ tr, td, th {
width: 30%; width: 30%;
padding-right: 0; padding-right: 0;
} }
@media (min-width: 768px) AND (max-width: 926px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
@media (max-width: 430px) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
}
} }
.size { .size {
@@ -214,10 +189,10 @@ tr, td, th {
.health { .health {
width: 10%; width: 10%;
@media (max-width: 1105px) { @media (max-width: 1000px) {
width: 13%; width: 13%;
} }
@media (max-width: 560px) { @media (max-width: 950px) {
display: none; display: none;
} }
@@ -227,7 +202,7 @@ tr, td, th {
} }
.health.widget { .health.widget {
width: 25%; width: 25%;
@media (max-width: 1105px) { @media (max-width: 1000px) {
display: none; display: none;
} }
@media (max-width: 767px) { @media (max-width: 767px) {

View File

@@ -1,42 +0,0 @@
<div class="clock-face" [style]="faceStyle">
<ng-content></ng-content>
<svg
class="cut-out"
width="384"
height="384"
viewBox="0 0 384 384"
>
<g>
<path
class="face"
d="M 0,0 V 384 H 384 V 0 Z M 192,15 A 177,177 0 0 1 369,192 177,177 0 0 1 192,369 177,177 0 0 1 15,192 177,177 0 0 1 192,15 Z"
/>
</g>
</svg>
<svg
class="demo-dial"
width="384"
height="384"
viewBox="0 0 384 384"
>
<defs>
<pattern id="dial-gradient" patternUnits="userSpaceOnUse" width="384" height="384">
<image class="dial-gradient-img" href="/resources/clock/gradient.png" x="0" y="0" width="384" height="384" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
</pattern>
</defs>
<path *ngFor="let angle of minorTicks" class="tick minor" d="M 192,27 v 10" [style.transform]="'rotate(' + angle + 'deg)'"/>
<path *ngFor="let angle of majorTicks" class="tick major" d="M 192,27 v 18" [style.transform]="'rotate(' + angle + 'deg)'"/>
<ng-container *ngFor="let segment of segments; trackBy: trackBySegment">
<path class="block-segment" [attr.d]="segment.path" />
<!-- <circle class="segment-mark start" [attr.cx]="segment.start.x" [attr.cy]="segment.start.y" r="2" style="fill:green;stroke:white;stroke-width:1px;" />
<circle class="segment-mark end" [attr.cx]="segment.end.x" [attr.cy]="segment.end.y" r="2" style="fill:red;stroke:white;stroke-width:1px;" /> -->
</ng-container>
<!-- <polyline points="468.750,82.031 468.750,35 " id="polyline322" style="fill:none;stroke:#ffffff;stroke-width:4.84839;stroke-dasharray:none;stroke-opacity:1" transform="matrix(0.41250847,0,0,0.93092534,-1.3627708,-32.692008)" /> -->
<path class="tick very major" d="M 192,0 v 45" />
<path id="hour" class="gnomon hour" d="M 178,3 206,3 192,40 Z" [style.transform]="'rotate(' + (hours * 30) + 'deg)'" />
<path id="minute" class="gnomon minute" d="M 180,4 204,4 192,38 Z" [style.transform]="'rotate(' + (minutes * 6) + 'deg)'" />
</svg>
</div>

View File

@@ -1,69 +0,0 @@
.clock-face {
position: relative;
height: 84.375%;
margin: auto;
overflow: hidden;
.cut-out, .demo-dial {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
.face {
fill: #11131f;
}
}
.gnomon {
transform-origin: center;
stroke-linejoin: round;
&.minute {
fill:#80C2E1;
stroke:#80C2E1;
stroke-width: 2px;
}
&.hour {
fill: #105fb0;
stroke: #105fb0;
stroke-width: 6px;
}
}
.tick {
transform-origin: center;
fill: none;
stroke: white;
stroke-width: 2px;
stroke-linecap: butt;
&.minor {
stroke-opacity: 0.5;
}
&.very.major {
stroke-width: 4px;
}
}
.block-segment {
fill: none;
stroke: url(#dial-gradient);
stroke-width: 18px;
}
.dial-segment {
fill: none;
stroke: white;
stroke-width: 2px;
}
.dial-gradient-img {
transform-origin: center;
}
}

View File

@@ -1,148 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-clock-face',
templateUrl: './clock-face.component.html',
styleUrls: ['./clock-face.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
@Input() size: number = 300;
blocksSubscription: Subscription;
timeSubscription: Subscription;
faceStyle;
dialPath;
blockTimes = [];
segments = [];
hours: number = 0;
minutes: number = 0;
minorTicks: number[] = [];
majorTicks: number[] = [];
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
private cd: ChangeDetectorRef
) {
this.updateTime();
this.makeTicks();
}
ngOnInit(): void {
this.timeSubscription = timer(0, 250).pipe(
tap(() => {
this.updateTime();
})
).subscribe();
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.blockTimes.push([block.height, new Date(block.timestamp * 1000)]);
// using block-reported times, so ensure they are sorted chronologically
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
this.updateSegments();
}
});
}
ngOnChanges(): void {
this.faceStyle = {
width: `${this.size}px`,
height: `${this.size}px`,
};
}
ngOnDestroy(): void {
this.timeSubscription.unsubscribe();
}
updateTime(): void {
const now = new Date();
const seconds = now.getSeconds() + (now.getMilliseconds() / 1000);
this.minutes = (now.getMinutes() + (seconds / 60)) % 60;
this.hours = now.getHours() + (this.minutes / 60);
this.updateSegments();
}
updateSegments(): void {
const now = new Date();
this.blockTimes = this.blockTimes.filter(time => (now.getTime() - time[1].getTime()) <= 3600000);
const tail = new Date(now.getTime() - 3600000);
const hourStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours());
const times = [
['start', tail],
...this.blockTimes,
['end', now],
];
const minuteTimes = times.map(time => {
return [time[0], (time[1].getTime() - hourStart.getTime()) / 60000];
});
this.segments = [];
const r = 174;
const cx = 192;
const cy = cx;
for (let i = 1; i < minuteTimes.length; i++) {
const arc = this.getArc(minuteTimes[i-1][1], minuteTimes[i][1], r, cx, cy);
if (arc) {
arc.id = minuteTimes[i][0];
this.segments.push(arc);
}
}
const arc = this.getArc(minuteTimes[0][1], minuteTimes[1][1], r, cx, cy);
if (arc) {
this.dialPath = arc.path;
}
this.cd.markForCheck();
}
getArc(startTime, endTime, r, cx, cy): any {
const startDegrees = (startTime + 0.2) * 6;
const endDegrees = (endTime - 0.2) * 6;
const start = this.getPointOnCircle(startDegrees, r, cx, cy);
const end = this.getPointOnCircle(endDegrees, r, cx, cy);
const arcLength = endDegrees - startDegrees;
// merge gaps and omit lines shorter than 1 degree
if (arcLength >= 1) {
const path = `M ${start.x} ${start.y} A ${r} ${r} 0 ${arcLength > 180 ? 1 : 0} 1 ${end.x} ${end.y}`;
return {
path,
start,
end
};
} else {
return null;
}
}
getPointOnCircle(deg, r, cx, cy) {
const modDeg = ((deg % 360) + 360) % 360;
const rad = (modDeg * Math.PI) / 180;
return {
x: cx + (r * Math.sin(rad)),
y: cy - (r * Math.cos(rad)),
};
}
makeTicks() {
this.minorTicks = [];
this.majorTicks = [];
for (let i = 1; i < 60; i++) {
if (i % 5 === 0) {
this.majorTicks.push(i * 6);
} else {
this.minorTicks.push(i * 6);
}
}
}
trackBySegment(index: number, segment) {
return segment.id;
}
}

View File

@@ -1 +0,0 @@
<app-clock mode="mempool"></app-clock>

View File

@@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mempool',
templateUrl: './clock-mempool.component.html',
})
export class ClockMempoolComponent {}

View File

@@ -1 +0,0 @@
<app-clock mode="block"></app-clock>

View File

@@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mined',
templateUrl: './clock-mined.component.html',
})
export class ClockMinedComponent {}

View File

@@ -1,67 +0,0 @@
<div class="clock-wrapper" [style]="wrapperStyle">
<div class="clockchain-bar" [style.height]="chainHeight + 'px'">
<div class="clockchain">
<app-clockchain [width]="chainWidth" [height]="chainHeight" [mode]="mode"></app-clockchain>
</div>
</div>
<div class="clock-face">
<app-clock-face [size]="clockSize">
<div class="block-wrapper">
<ng-container *ngIf="block && block.height >= 0">
<ng-container *ngIf="mode === 'block'; else mempoolMode;">
<div class="block-cube">
<div class="side top"></div>
<div class="side bottom"></div>
<div class="side right" [style]="blockStyle"></div>
<div class="side left" [style]="blockStyle"></div>
<div class="side front" [style]="blockStyle"></div>
<div class="side back" [style]="blockStyle"></div>
</div>
</ng-container>
<ng-template #mempoolMode>
<div class="block-sizer" [style]="blockSizerStyle">
<app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
</div>
</ng-template>
<div class="fader"></div>
<div class="title-wrapper">
<h1 class="block-height">{{ block.height }}</h1>
</div>
</ng-container>
</div>
</app-clock-face>
</div>
<ng-container *ngIf="!hideStats">
<div class="stats top left">
<p class="label" i18n="clock.fiat-price">fiat price</p>
<p>
<app-fiat [value]="100000000" digitsInfo="1.2-2" colorClass="white-color"></app-fiat>
</p>
</div>
<div class="stats top right">
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee + 300 }} sat/vB</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
<p [innerHTML]="block.size | bytes: 2"></p>
<p class="label" i18n="clock.block-size">block size</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
<p class="force-wrap">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
</p>
</div>
<ng-container *ngIf="mempoolInfo$ | async as mempoolInfo;">
<div *ngIf="mode === 'mempool'" class="stats bottom left">
<p [innerHTML]="mempoolInfo.usage | bytes: 0"></p>
<p class="label" i18n="dashboard.memory-usage|Memory usage">memory usage</p>
</div>
<div *ngIf="mode === 'mempool'" class="stats bottom right">
<p>{{ mempoolInfo.size | number }}</p>
<p class="label" i18n="dashboard.unconfirmed|Unconfirmed count">unconfirmed</p>
</div>
</ng-container>
</ng-container>
</div>

View File

@@ -1,190 +0,0 @@
.clock-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
--chain-height: 60px;
--clock-width: 300px;
.clockchain-bar, .clock-face {
flex-shrink: 0;
flex-grow: 0;
}
.clockchain-bar {
position: relative;
width: 100%;
height: 15.625%;
z-index: 2;
// overflow: hidden;
// background: #1d1f31;
// box-shadow: 0 0 15px #000;
}
.clock-face {
position: relative;
height: 84.375%;
margin: auto;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
.stats {
position: absolute;
z-index: 3;
p {
margin: 0;
font-size: calc(0.055 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width));
opacity: 0.8;
&.force-wrap {
word-spacing: 10000px;
}
::ng-deep .symbol {
font-size: inherit;
color: white;
}
}
.label {
font-size: calc(0.04 * var(--clock-width));
line-height: calc(0.05 * var(--clock-width));
}
&.top {
top: calc(var(--chain-height) + 2%);
}
&.bottom {
bottom: 2%;
}
&.left {
left: 5%;
}
&.right {
right: 5%;
text-align: end;
text-align: right;
}
}
}
.title-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.block-height {
font-size: calc(0.2 * var(--clock-width));
padding: 0;
margin: 0;
background: radial-gradient(rgba(0,0,0,0.5), transparent 67%);
padding: calc(0.05 * var(--clock-width)) calc(0.15 * var(--clock-width));
}
}
.block-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
.block-sizer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.fader {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: radial-gradient(transparent 0%, transparent 44%, #11131f 58%, #11131f 100%);
}
.block-cube {
--side-width: calc(0.4 * var(--clock-width));
--half-side: calc(0.2 * var(--clock-width));
--neg-half-side: calc(-0.2 * var(--clock-width));
transform-style: preserve-3d;
animation: block-spin 60s infinite linear;
position: absolute;
z-index: -1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: var(--side-width);
height: var(--side-width);
.side {
width: var(--side-width);
height: var(--side-width);
line-height: 100px;
text-align: center;
background: #232838;
display: block;
position: absolute;
}
.side.top {
transform: rotateX(90deg);
margin-top: var(--neg-half-side);
}
.side.bottom {
background: #105fb0;
transform: rotateX(-90deg);
margin-top: var(--half-side);
}
.side.right {
transform: rotateY(90deg);
margin-left: var(--half-side);
}
.side.left {
transform: rotateY(-90deg);
margin-left: var(--neg-half-side);
}
.side.front {
transform: translateZ(var(--half-side));
}
.side.back {
transform: translateZ(var(--neg-half-side));
}
}
}
@keyframes block-spin {
0% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(0deg);}
100% {transform: translate(-50%, -50%) rotateX(-20deg) rotateY(-360deg);}
}

View File

@@ -1,105 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-clock',
templateUrl: './clock.component.html',
styleUrls: ['./clock.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockComponent implements OnInit {
@Input() mode: 'block' | 'mempool' = 'block';
hideStats: boolean = false;
blocksSubscription: Subscription;
recommendedFees$: Observable<Recommendedfees>;
mempoolInfo$: Observable<MempoolInfo>;
block: BlockExtended;
clockSize: number = 300;
chainWidth: number = 384;
chainHeight: number = 60;
blockStyle;
blockSizerStyle;
wrapperStyle;
limitWidth: number;
limitHeight: number;
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
'liquidtestnet': ['#494a4a', '#272e46'],
testnet: ['#1d486f', '#183550'],
signet: ['#6f1d5d', '#471850'],
};
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
private route: ActivatedRoute,
private cd: ChangeDetectorRef,
) {
this.route.queryParams.subscribe((params) => {
this.hideStats = params && params.stats === 'false';
this.limitWidth = Number.parseInt(params.width) || null;
this.limitHeight = Number.parseInt(params.height) || null;
});
}
ngOnInit(): void {
this.resizeCanvas();
this.websocketService.want(['blocks']);
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.block = block;
this.blockStyle = this.getStyleForBlock(this.block);
this.cd.markForCheck();
}
});
this.recommendedFees$ = this.stateService.recommendedFees$;
this.mempoolInfo$ = this.stateService.mempoolInfo$;
}
getStyleForBlock(block: BlockExtended) {
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
return {
background: `repeating-linear-gradient(
#2d3348,
#2d3348 ${greenBackgroundHeight}%,
${this.gradientColors[''][0]} ${Math.max(greenBackgroundHeight, 0)}%,
${this.gradientColors[''][1]} 100%
)`,
};
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
const windowWidth = this.limitWidth || window.innerWidth;
const windowHeight = this.limitHeight || window.innerHeight;
this.chainWidth = windowWidth;
this.chainHeight = Math.max(60, windowHeight / 8);
this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));
const size = Math.ceil(this.clockSize / 75) * 75;
const margin = (this.clockSize - size) / 2;
this.blockSizerStyle = {
transform: `translate(${margin}px, ${margin}px)`,
width: `${size}px`,
height: `${size}px`,
};
this.wrapperStyle = {
'--clock-width': `${this.clockSize}px`,
'--chain-height': `${this.chainHeight}px`,
'width': this.limitWidth ? `${this.limitWidth}px` : undefined,
'height': this.limitHeight ? `${this.limitHeight}px` : undefined,
};
this.cd.markForCheck();
}
}

View File

@@ -1,28 +0,0 @@
<div
class="text-center" class="blockchain-wrapper" [class.time-ltr]="timeLtr" #container
[class.ltr-transition]="ltrTransitionEnabled" [style.width]="width + 'px'" [style.height]="height + 'px'"
>
<div class="position-container" [ngClass]="network ? network : ''" [style.top]="(height / 3) + 'px'">
<span>
<div class="blocks-wrapper">
<app-mempool-blocks [minimal]="true" [count]="mempoolBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'mempool' ? 1 : 0"></app-mempool-blocks>
<app-blockchain-blocks [minimal]="true" [count]="blockchainBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'block' ? -1 : 0"></app-blockchain-blocks>
</div>
<div class="divider" [style.top]="-(height / 6) + 'px'">
<svg
viewBox="0 0 2 175"
[style.width]="'2px'"
[style.height]="(5 * height / 6) + 'px'"
>
<line
class="divider-line"
x0="0"
x1="0"
y0="0"
y1="175px"
></line>
</svg>
</div>
</span>
</div>
</div>

View File

@@ -1,94 +0,0 @@
.divider {
position: absolute;
left: -0.5px;
top: 0;
.divider-line {
stroke: white;
stroke-width: 4px;
stroke-linecap: butt;
stroke-dasharray: 25px 25px;
}
}
.blockchain-wrapper {
height: 100%;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}
.position-container {
position: absolute;
left: 50%;
top: 0;
}
.black-background {
background-color: #11131f;
z-index: 100;
position: relative;
}
.scroll-spacer {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 1px;
pointer-events: none;
}
.loading-block {
position: absolute;
text-align: center;
margin: auto;
width: 300px;
left: -150px;
top: 0px;
}
.time-toggle {
color: white;
font-size: 0.8rem;
position: absolute;
bottom: -1.8em;
left: 1px;
transform: translateX(-50%);
background: none;
border: none;
outline: none;
margin: 0;
padding: 0;
}
.blockchain-wrapper.ltr-transition .blocks-wrapper,
.blockchain-wrapper.ltr-transition .position-container,
.blockchain-wrapper.ltr-transition .time-toggle {
transition: transform 1s;
}
.blockchain-wrapper.time-ltr {
.blocks-wrapper {
transform: scaleX(-1);
}
.time-toggle {
transform: translateX(-50%) scaleX(-1);
}
}
:host-context(.ltr-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: ltr;
}
}
:host-context(.rtl-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: rtl;
}
}

View File

@@ -1,73 +0,0 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-clockchain',
templateUrl: './clockchain.component.html',
styleUrls: ['./clockchain.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
@Input() width: number = 300;
@Input() height: number = 60;
@Input() mode: 'mempool' | 'block';
mempoolBlocks: number = 3;
blockchainBlocks: number = 6;
blockWidth: number = 50;
dividerStyle;
network: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
ltrTransitionEnabled = false;
connectionStateSubscription: Subscription;
loadingTip: boolean = true;
connected: boolean = true;
constructor(
public stateService: StateService,
private cd: ChangeDetectorRef,
) {}
ngOnInit() {
this.ngOnChanges();
this.network = this.stateService.network;
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.connectionStateSubscription = this.stateService.connectionState$.subscribe(state => {
this.connected = (state === 2);
});
firstValueFrom(this.stateService.chainTip$).then(() => {
this.loadingTip = false;
});
}
ngOnChanges() {
this.blockWidth = Math.floor(7 * this.height / 12);
this.mempoolBlocks = Math.floor(((this.width / 2) - (this.blockWidth * 0.32)) / (1.24 * this.blockWidth));
this.blockchainBlocks = this.mempoolBlocks;
this.dividerStyle = {
width: '2px',
height: `${this.height}px`,
};
this.cd.markForCheck();
}
ngOnDestroy() {
this.timeLtrSubscription.unsubscribe();
this.connectionStateSubscription.unsubscribe();
}
trackByPageFn(index: number, item: { index: number }) {
return item.index;
}
toggleTimeDirection() {
this.ltrTransitionEnabled = true;
this.stateService.timeLtr.next(!this.timeLtr);
}
}

View File

@@ -2,19 +2,18 @@
<table class="table latest-adjustments"> <table class="table latest-adjustments">
<thead> <thead>
<tr> <tr>
<th class="" i18n="block.height">Height</th> <th class="d-none d-md-block" i18n="block.height">Height</th>
<th class="date text-left" i18n="mining.adjusted">Adjusted</th> <th i18n="mining.adjusted" class="text-left">Adjusted</th>
<th class="text-right" i18n="mining.difficulty">Difficulty</th> <th i18n="mining.difficulty" class="text-right">Difficulty</th>
<th class="text-right" i18n="mining.change">Change</th> <th i18n="mining.change" class="text-right">Change</th>
</tr> </tr>
</thead> </thead>
<tbody *ngIf="(hashrateObservable$ | async) as data"> <tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data"> <tr *ngFor="let diffChange of data">
<td class=""> <td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a> }}</a></td>
</td> <td class="text-left">
<td class="date text-left"> <app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1"></app-time>
</td> </td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td> <td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'"> <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
@@ -24,8 +23,8 @@
</tbody> </tbody>
<tbody *ngIf="isLoading"> <tbody *ngIf="isLoading">
<tr *ngFor="let item of [1,2,3,4,5,6]"> <tr *ngFor="let item of [1,2,3,4,5,6]">
<td class=""><span class="skeleton-loader"></span></td> <td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
<td class="date text-left"><span class="skeleton-loader w-75"></span></td> <td class="text-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td> <td class="text-right"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td> <td class="text-right"><span class="skeleton-loader w-75"></span></td>
</tr> </tr>

View File

@@ -17,12 +17,3 @@
} }
} }
} }
.date {
@media (min-width: 767px) AND (max-width: 991px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}

View File

@@ -10,7 +10,7 @@
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div> </div>
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div> <div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5> <h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
@@ -47,14 +47,13 @@
</div> </div>
</div> </div>
<div class="item" *ngIf="showHalving"> <div class="item" *ngIf="showHalving">
<h5 class="card-title" i18n="difficulty-box.next-halving" i18n-ngbTooltip="difficulty-box.next-halving" <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
ngbTooltip="Next Halving" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Next Halving</h5>
<div class="card-text"> <div class="card-text">
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container> <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template> <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template> <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
</div> </div>
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true" [precision]="1"></app-time></div> <div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
</div> </div>
</div> </div>
</div> </div>
@@ -78,7 +77,7 @@
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5> <h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
<div class="card-text"> <div class="card-text">
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>
<div class="skeleton-loader"></div> <div class="skeleton-loader"></div>

View File

@@ -10,7 +10,6 @@
.item { .item {
padding: 0 5px; padding: 0 5px;
width: 100%; width: 100%;
max-width: 150px;
&:nth-child(1) { &:nth-child(1) {
display: none; display: none;
@media (min-width: 485px) { @media (min-width: 485px) {
@@ -86,9 +85,6 @@
.card-title { .card-title {
color: #4a68b9; color: #4a68b9;
font-size: 1rem; font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.progress { .progress {
@@ -156,5 +152,4 @@
.symbol { .symbol {
font-size: 13px; font-size: 13px;
white-space: nowrap;
} }

View File

@@ -83,8 +83,4 @@ export class DifficultyMiningComponent implements OnInit {
}) })
); );
} }
isEllipsisActive(e): boolean {
return (e.offsetWidth < e.scrollWidth);
}
} }

View File

@@ -37,12 +37,12 @@
<div class="difficulty-stats"> <div class="difficulty-stats">
<div class="item"> <div class="item">
<div class="card-text"> <div class="card-text">
~<app-time [time]="epochData.timeAvg / 1000" [fractionDigits]="1"></app-time> ~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
</div> </div>
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div> <div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
</div> </div>
<div class="item"> <div class="item">
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text bigger" [ngStyle]="{'color': epochData.colorAdjustments}"> <div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" > <span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon> <fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
</span> </span>
@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true" [precision]="1"></app-time></div> <div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
<div class="symbol"> <div class="symbol">
{{ epochData.retargetDateString }} {{ epochData.retargetDateString }}
</div> </div>

View File

@@ -30,14 +30,9 @@
} }
} }
.card-text { .card-text {
font-size: 18px; font-size: 20px;
margin: auto; margin: auto;
position: relative; position: relative;
margin-bottom: 0.2rem;
&.bigger {
font-size: 20px;
margin-bottom: 0;
}
} }
} }
@@ -165,7 +160,6 @@
.symbol { .symbol {
font-size: 13px; font-size: 13px;
white-space: nowrap;
} }
.epoch-progress { .epoch-progress {

View File

@@ -26,23 +26,15 @@ export class FeesBoxComponent implements OnInit {
this.recommendedFees$ = this.stateService.recommendedFees$ this.recommendedFees$ = this.stateService.recommendedFees$
.pipe( .pipe(
tap((fees) => { tap((fees) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.economyFee >= feeLvl); let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]); const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.hourFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const lowColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.halfHourFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const medColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl); feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex; feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const highColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]); const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
this.gradient = `linear-gradient(to right, ${lowColor} 0%, ${medColor} 50%, ${highColor} 100%)`; this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
this.noPriority = startColor; this.noPriority = startColor;
} }
) )

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,19 +18,16 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex;
flex: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-bottom: 20px; padding-bottom: 20px;

View File

@@ -4,9 +4,6 @@
@media (min-width: 465px) { @media (min-width: 465px) {
font-size: 20px; font-size: 20px;
} }
@media (min-width: 992px) {
height: 40px;
}
} }
.main-title { .main-title {
@@ -21,20 +18,18 @@
} }
.full-container { .full-container {
display: flex;
flex-direction: column;
padding: 0px 15px; padding: 0px 15px;
width: 100%; width: 100%;
height: calc(100vh - 250px); min-height: 500px;
@media (min-width: 992px) { height: calc(100% - 150px);
height: calc(100vh - 150px); @media (max-width: 992px) {
} padding-bottom: 100px;
};
} }
.chart { .chart {
display: flex;
flex: 1;
width: 100%; width: 100%;
height: 100%;
padding-bottom: 20px; padding-bottom: 20px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) { @media (max-width: 992px) {

View File

@@ -90,12 +90,9 @@
</nav> </nav>
</header> </header>
<app-testnet-alert *ngIf="network.val === 'liquidtestnet'"></app-testnet-alert>
<br /> <br />
<router-outlet></router-outlet> <router-outlet></router-outlet>
<app-global-footer></app-global-footer> <br>
</ng-container> </ng-container>

View File

@@ -62,11 +62,10 @@
</nav> </nav>
</header> </header>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert> <br />
<main style="margin-top: 24px;"> <router-outlet></router-outlet>
<router-outlet></router-outlet>
</main> <br>
<app-global-footer></app-global-footer>
</ng-container> </ng-container>

Some files were not shown because too many files have changed in this diff Show More