Compare commits

..

2 Commits

Author SHA1 Message Date
Mononaut
cdfee53cf5 Use minfee second node to supply alt rbf policy mempool txs 2023-01-17 19:25:53 -06:00
Mononaut
be27e3df52 check for txs in alternative backend mempool 2023-01-17 19:25:53 -06:00
336 changed files with 31111 additions and 70075 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
backend/src/api/database-migration.ts @wiz @softsimon

View File

@@ -9,7 +9,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy: strategy:
matrix: matrix:
node: ["16.16.0", "18.14.1"] node: ["16.16.0", "18.5.0"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
@@ -55,7 +55,7 @@ jobs:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy: strategy:
matrix: matrix:
node: ["16.16.0", "18.14.1"] node: ["16.15.0", "18.5.0"]
flavor: ["dev", "prod"] flavor: ["dev", "prod"]
fail-fast: false fail-fast: false
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"

View File

@@ -1,11 +1,8 @@
name: Cypress Tests name: Cypress Tests
on: on:
push:
branches: [master]
pull_request: pull_request:
types: [opened, synchronize] types: [opened, review_requested, synchronize]
jobs: jobs:
cypress: cypress:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"

View File

@@ -1,26 +0,0 @@
name: 'Print images digest'
on:
workflow_dispatch:
inputs:
version:
description: 'Image Version'
required: false
default: 'latest'
type: string
jobs:
print-images-sha:
runs-on: 'ubuntu-latest'
name: Print digest for images
steps:
- name: Checkout
uses: actions/checkout@v3
with:
path: digest
- name: Run script
working-directory: digest
run: |
sh ./docker/scripts/get_image_digest.sh $VERSION
env:
VERSION: ${{ github.event.inputs.version }}

View File

@@ -1,5 +1,4 @@
{ {
"editor.tabSize": 2, "editor.tabSize": 2,
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.tsdk": "./backend/node_modules/typescript/lib" "typescript.tsdk": "./backend/node_modules/typescript/lib"
} }

View File

@@ -1,5 +1,5 @@
The Mempool Open Source Project The Mempool Open Source Project
Copyright (c) 2019-2023 The Mempool Open Source Project Developers Copyright (c) 2019-2022 The Mempool Open Source Project Developers
This program is free software; you can redistribute it and/or modify it under This program is free software; you can redistribute it and/or modify it under
the terms of (at your option) either: the terms of (at your option) either:

View File

@@ -1,7 +1,5 @@
# 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/232186/222445818-234aa6c9-c233-4c52-b3f0-e32b8232893b.mp4
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.

View File

@@ -160,7 +160,7 @@ npm install -g ts-node nodemon
Then, run the watcher: Then, run the watcher:
``` ```
nodemon src/index.ts --ignore cache/ nodemon src/index.ts --ignore cache/ --ignore pools.json
``` ```
`nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`. `nodemon` should be in npm's global binary folder. If needed, you can determine where that is with `npm -g bin`.
@@ -171,84 +171,50 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
Run bitcoind on regtest: Run bitcoind on regtest:
``` ```
bitcoind -regtest bitcoind -regtest -rpcport=8332
``` ```
Create a new wallet, if needed: Create a new wallet, if needed:
``` ```
bitcoin-cli -regtest createwallet test bitcoin-cli -regtest -rpcport=8332 createwallet test
``` ```
Load wallet (this command may take a while if you have lot of UTXOs): Load wallet (this command may take a while if you have lot of UTXOs):
``` ```
bitcoin-cli -regtest loadwallet test bitcoin-cli -regtest -rpcport=8332 loadwallet test
``` ```
Get a new address: Get a new address:
``` ```
address=$(bitcoin-cli -regtest getnewaddress) address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
``` ```
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min): Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
``` ```
bitcoin-cli -regtest generatetoaddress 101 $address bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address
``` ```
Send 0.1 BTC at 5 sat/vB to another address: Send 0.1 BTC at 5 sat/vB to another address:
``` ```
bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5 ./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
``` ```
See more example of `sendtoaddress`: See more example of `sendtoaddress`:
``` ```
bitcoin-cli sendtoaddress # will print the help ./src/bitcoin-cli sendtoaddress # will print the help
``` ```
Mini script to generate random network activity (random TX count with random tx fee-rate). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other. Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
``` ```
#!/bin/bash #!/bin/bash
address=$(bitcoin-cli -regtest getnewaddress) address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
bitcoin-cli -regtest generatetoaddress 101 $address
for i in {1..1000000} for i in {1..1000000}
do do
for y in $(seq 1 "$(jot -r 1 1 1000)") ./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
do
bitcoin-cli -regtest -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
done
bitcoin-cli -regtest generatetoaddress 1 $address
sleep 5
done done
``` ```
Generate block at regular interval (every 10 seconds in this example): Generate block at regular interval (every 10 seconds in this example):
``` ```
watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address" watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
``` ```
### Mining pools update
By default, mining pools will be not automatically updated regularly (`config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` is set to `false`).
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
### Re-index tables
You can manually force the nodejs backend to drop all data from a specified set of tables for future re-index. This is mostly useful for the mining dashboard and the lightning explorer.
Use the `--reindex` command to specify a list of comma separated table which will be truncated at start. Note that a 5 seconds delay will be observed before truncating tables in order to give you a chance to cancel (CTRL+C) in case of misuse of the command.
Usage:
```
npm run start --reindex=blocks,hashrates
```
Example output:
```
Feb 13 14:55:27 [63246] WARN: <lightning> Indexed data for "hashrates" tables will be erased in 5 seconds (using '--reindex')
Feb 13 14:55:32 [63246] NOTICE: <lightning> Table hashrates has been truncated
```
Reference: https://github.com/mempool/mempool/pull/1269

View File

@@ -15,6 +15,7 @@
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 11000, "INDEXING_BLOCKS_AMOUNT": 11000,
"BLOCKS_SUMMARIES_INDEXING": false, "BLOCKS_SUMMARIES_INDEXING": false,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false, "USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [], "EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1, "EXTERNAL_MAX_RETRY": 1,
@@ -22,12 +23,12 @@
"USER_AGENT": "mempool", "USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug", "STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false, "AUTOMATIC_BLOCK_REINDEXING": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"AUDIT": false,
"ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false "CPFP_INDEXING": false,
"RBF_DUAL_NODE": false
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View File

@@ -27,7 +27,7 @@
"package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps", "package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps",
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)", "package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=16384 dist/index.js", "start-production": "node --max-old-space-size=4096 dist/index.js",
"test": "./node_modules/.bin/jest --coverage", "test": "./node_modules/.bin/jest --coverage",
"lint": "./node_modules/.bin/eslint . --ext .ts", "lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",

View File

@@ -3,11 +3,12 @@
"ENABLED": true, "ENABLED": true,
"NETWORK": "__MEMPOOL_NETWORK__", "NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__", "BACKEND": "__MEMPOOL_BACKEND__",
"ENABLED": true,
"BLOCKS_SUMMARIES_INDEXING": true, "BLOCKS_SUMMARIES_INDEXING": true,
"HTTP_PORT": 1, "HTTP_PORT": 1,
"SPAWN_CLUSTER_PROCS": 2, "SPAWN_CLUSTER_PROCS": 2,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__", "API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"AUTOMATIC_BLOCK_REINDEXING": false, "AUTOMATIC_BLOCK_REINDEXING": true,
"POLL_RATE_MS": 3, "POLL_RATE_MS": 3,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__", "CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CLEAR_PROTECTION_MINUTES": 4, "CLEAR_PROTECTION_MINUTES": 4,
@@ -15,6 +16,7 @@
"BLOCK_WEIGHT_UNITS": 6, "BLOCK_WEIGHT_UNITS": 6,
"INITIAL_BLOCKS_AMOUNT": 7, "INITIAL_BLOCKS_AMOUNT": 7,
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"PRICE_FEED_UPDATE_INTERVAL": 9,
"USE_SECOND_NODE_FOR_MINFEE": 10, "USE_SECOND_NODE_FOR_MINFEE": 10,
"EXTERNAL_ASSETS": 11, "EXTERNAL_ASSETS": 11,
"EXTERNAL_MAX_RETRY": 12, "EXTERNAL_MAX_RETRY": 12,
@@ -24,11 +26,10 @@
"INDEXING_BLOCKS_AMOUNT": 14, "INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__", "POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__", "POOLS_JSON_URL": "__POOLS_JSON_URL__",
"AUDIT": "__MEMPOOL_AUDIT__", "ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__", "ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__", "CPFP_INDEXING": "__CPFP_INDEXING__",
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__", "RBF_DUAL_NODE": "__RBF_DUAL_NODE__"
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__"
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",

View File

@@ -23,11 +23,9 @@ describe('Mempool Difficulty Adjustment', () => {
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
previousTime: 1660820820,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: 0, timeOffset: 0,
expectedBlocks: 161.68833333333333,
}, },
], ],
[ // Vector 2 (testnet) [ // Vector 2 (testnet)
@@ -45,13 +43,11 @@ describe('Mempool Difficulty Adjustment', () => {
estimatedRetargetDate: 1661895424692, estimatedRetargetDate: 1661895424692,
remainingBlocks: 1834, remainingBlocks: 1834,
remainingTime: 977591692, remainingTime: 977591692,
previousTime: 1660820820,
previousRetarget: 0.6280047707459726, previousRetarget: 0.6280047707459726,
nextRetargetHeight: 751968, nextRetargetHeight: 751968,
timeAvg: 533038, timeAvg: 533038,
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only) timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes // If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
expectedBlocks: 161.68833333333333,
}, },
], ],
] as [[number, number, number, number, string, number], DifficultyAdjustment][]; ] as [[number, number, number, number, string, number], DifficultyAdjustment][];

View File

@@ -29,6 +29,7 @@ describe('Mempool Backend Config', () => {
INITIAL_BLOCKS_AMOUNT: 8, INITIAL_BLOCKS_AMOUNT: 8,
MEMPOOL_BLOCKS_AMOUNT: 8, MEMPOOL_BLOCKS_AMOUNT: 8,
INDEXING_BLOCKS_AMOUNT: 11000, INDEXING_BLOCKS_AMOUNT: 11000,
PRICE_FEED_UPDATE_INTERVAL: 600,
USE_SECOND_NODE_FOR_MINFEE: false, USE_SECOND_NODE_FOR_MINFEE: false,
EXTERNAL_ASSETS: [], EXTERNAL_ASSETS: [],
EXTERNAL_MAX_RETRY: 1, EXTERNAL_MAX_RETRY: 1,
@@ -36,12 +37,11 @@ describe('Mempool Backend Config', () => {
USER_AGENT: 'mempool', USER_AGENT: 'mempool',
STDOUT_LOG_MIN_PRIORITY: 'debug', STDOUT_LOG_MIN_PRIORITY: 'debug',
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
AUDIT: false,
ADVANCED_GBT_AUDIT: false, ADVANCED_GBT_AUDIT: false,
ADVANCED_GBT_MEMPOOL: false, ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false, CPFP_INDEXING: false,
MAX_BLOCKS_BULK_QUERY: 0, RBF_DUAL_NODE: false,
}); });
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 });
@@ -106,13 +106,6 @@ describe('Mempool Backend Config', () => {
BISQ_URL: 'https://bisq.markets/api', BISQ_URL: 'https://bisq.markets/api',
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api' BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}); });
expect(config.MAXMIND).toStrictEqual({
ENABLED: true,
GEOLITE2_CITY: './backend/GeoIP/GeoLite2-City.mmdb',
GEOLITE2_ASN: './backend/GeoIP/GeoLite2-ASN.mmdb',
GEOIP2_ISP: ''
});
}); });
}); });

View File

@@ -0,0 +1,175 @@
import config from '../config';
import { TransactionExtended } from '../mempool.interfaces';
import logger from '../logger';
import { Common } from './common';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import BitcoinApi from './bitcoin/bitcoin-api';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Mempool } from './mempool';
class AltMempool extends Mempool {
private bitcoinSecondApi: BitcoinApi;
constructor() {
super();
this.bitcoinSecondApi = new BitcoinApi(bitcoinSecondClient, this, bitcoinApi);
}
protected init(): void {
// override
}
public setOutOfSync(): void {
this.inSync = false;
}
public setMempool(mempoolData: { [txId: string]: TransactionExtended }): void {
this.mempoolCache = mempoolData;
}
public getFirstSeenForTransactions(txIds: string[]): number[] {
const txTimes: number[] = [];
txIds.forEach((txId: string) => {
const tx = this.mempoolCache[txId];
if (tx && tx.firstSeen) {
txTimes.push(tx.firstSeen);
} else {
txTimes.push(0);
}
});
return txTimes;
}
public async $updateMempool(): Promise<void> {
logger.debug(`Updating alternative mempool...`);
const start = new Date().getTime();
const currentMempoolSize = Object.keys(this.mempoolCache).length;
const transactions = await this.bitcoinSecondApi.$getRawMempool();
const diff = transactions.length - currentMempoolSize;
this.mempoolCacheDelta = Math.abs(diff);
const loadingMempool = this.mempoolCacheDelta > 100;
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await this.$fetchTransaction(txid);
this.mempoolCache[txid] = transaction;
if (loadingMempool && Object.keys(this.mempoolCache).length % 50 === 0) {
logger.info(`loaded ${Object.keys(this.mempoolCache).length}/${transactions.length} alternative mempool transactions`);
}
} catch (e) {
logger.debug(`Error finding transaction '${txid}' in the alternative mempool: ` + (e instanceof Error ? e.message : e));
}
}
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
break;
}
}
if (loadingMempool) {
logger.info(`loaded ${Object.keys(this.mempoolCache).length}/${transactions.length} alternative mempool transactions`);
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0
&& currentMempoolSize > 20000
&& transactions.length / currentMempoolSize <= 0.80
) {
this.mempoolProtection = 1;
setTimeout(() => {
this.mempoolProtection = 2;
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
const deletedTransactions: string[] = [];
if (this.mempoolProtection !== 1) {
this.mempoolProtection = 0;
// Index object for faster search
const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true);
// Flag transactions for lazy deletion
for (const tx in this.mempoolCache) {
if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) {
deletedTransactions.push(this.mempoolCache[tx].txid);
}
}
for (const txid of deletedTransactions) {
delete this.mempoolCache[txid];
}
}
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
const end = new Date().getTime();
const time = end - start;
logger.debug(`Alt mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
}
public getTransaction(txid: string): TransactionExtended {
return this.mempoolCache[txid] || null;
}
protected async $fetchTransaction(txid: string): Promise<TransactionExtended> {
const rawTx = await this.bitcoinSecondApi.$getRawTransaction(txid, false, true, false);
return this.extendTransaction(rawTx);
}
protected extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {
// @ts-ignore
return transaction;
}
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / (transaction.weight / 4));
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes,
}, transaction);
if (!transaction.status.confirmed) {
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
}
return transactionExtended;
}
public async $updateMemPoolInfo(): Promise<void> {
this.mempoolInfo = await this.$getMempoolInfo();
}
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
return this.mempoolInfo;
}
public getTxPerSecond(): number {
return this.txPerSecond;
}
public getVBytesPerSecond(): number {
return this.vBytesPerSecond;
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) {
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
}
}
}
protected updateTxPerSecond(): void {}
protected deleteExpiredTransactions(): void {}
protected $getMempoolInfo(): any {
return bitcoinSecondClient.getMempoolInfo();
}
}
export default new AltMempool();

View File

@@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
class Audit { class Audit {
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended }) auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
: { censored: string[], added: string[], fresh: string[], score: number, similarity: number } { : { censored: string[], added: string[], fresh: string[], score: number } {
if (!projectedBlocks?.[0]?.transactionIds || !mempool) { if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 }; return { censored: [], added: [], fresh: [], score: 0 };
} }
const matches: string[] = []; // present in both mined block and template const matches: string[] = []; // present in both mined block and template
@@ -16,8 +16,6 @@ class Audit {
const isCensored = {}; // missing, without excuse const isCensored = {}; // missing, without excuse
const isDisplaced = {}; const isDisplaced = {};
let displacedWeight = 0; let displacedWeight = 0;
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {}; const inBlock = {};
const inTemplate = {}; const inTemplate = {};
@@ -40,16 +38,11 @@ class Audit {
isCensored[txid] = true; isCensored[txid] = true;
} }
displacedWeight += mempool[txid].weight; displacedWeight += mempool[txid].weight;
} else {
matchedWeight += mempool[txid].weight;
} }
projectedWeight += mempool[txid].weight;
inTemplate[txid] = true; inTemplate[txid] = true;
} }
displacedWeight += (4000 - transactions[0].weight); displacedWeight += (4000 - transactions[0].weight);
projectedWeight += 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
@@ -126,16 +119,13 @@ class Audit {
} }
const numCensored = Object.keys(isCensored).length; const numCensored = Object.keys(isCensored).length;
const numMatches = matches.length - 1; // adjust for coinbase tx const score = matches.length > 0 ? (matches.length / (matches.length + numCensored)) : 0;
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
return { return {
censored: Object.keys(isCensored), censored: Object.keys(isCensored),
added, added,
fresh, fresh,
score, score
similarity,
}; };
} }
} }

View File

@@ -4,19 +4,20 @@ import EsploraApi from './esplora-api';
import BitcoinApi from './bitcoin-api'; import BitcoinApi from './bitcoin-api';
import ElectrumApi from './electrum-api'; import ElectrumApi from './electrum-api';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import mempool from '../mempool';
function bitcoinApiFactory(): AbstractBitcoinApi { function bitcoinApiFactory(): AbstractBitcoinApi {
switch (config.MEMPOOL.BACKEND) { switch (config.MEMPOOL.BACKEND) {
case 'esplora': case 'esplora':
return new EsploraApi(); return new EsploraApi();
case 'electrum': case 'electrum':
return new ElectrumApi(bitcoinClient); return new ElectrumApi(bitcoinClient, mempool);
case 'none': case 'none':
default: default:
return new BitcoinApi(bitcoinClient); return new BitcoinApi(bitcoinClient, mempool);
} }
} }
export const bitcoinCoreApi = new BitcoinApi(bitcoinClient); export const bitcoinCoreApi = new BitcoinApi(bitcoinClient, mempool);
export default bitcoinApiFactory(); export default bitcoinApiFactory();

View File

@@ -172,35 +172,4 @@ export namespace IBitcoinApi {
} }
} }
export interface BlockStats {
"avgfee": number;
"avgfeerate": number;
"avgtxsize": number;
"blockhash": string;
"feerate_percentiles": [number, number, number, number, number];
"height": number;
"ins": number;
"maxfee": number;
"maxfeerate": number;
"maxtxsize": number;
"medianfee": number;
"mediantime": number;
"mediantxsize": number;
"minfee": number;
"minfeerate": number;
"mintxsize": number;
"outs": number;
"subsidy": number;
"swtotal_size": number;
"swtotal_weight": number;
"swtxs": number;
"time": number;
"total_out": number;
"total_size": number;
"total_weight": number;
"totalfee": number;
"txs": number;
"utxo_increase": number;
"utxo_size_inc": number;
}
} }

View File

@@ -3,15 +3,18 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface'; import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; import { MempoolBlock, TransactionExtended } from '../../mempool.interfaces';
import { TransactionExtended } from '../../mempool.interfaces';
class BitcoinApi implements AbstractBitcoinApi { class BitcoinApi implements AbstractBitcoinApi {
private mempool: any = null;
private rawMempoolCache: IBitcoinApi.RawMempool | null = null; private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
protected bitcoindClient: any; protected bitcoindClient: any;
protected backupBitcoinApi: BitcoinApi;
constructor(bitcoinClient: any) { constructor(bitcoinClient: any, mempool: any, backupBitcoinApi: any = null) {
this.bitcoindClient = bitcoinClient; this.bitcoindClient = bitcoinClient;
this.mempool = mempool;
this.backupBitcoinApi = backupBitcoinApi;
} }
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block { static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
@@ -28,14 +31,13 @@ class BitcoinApi implements AbstractBitcoinApi {
size: block.size, size: block.size,
weight: block.weight, weight: block.weight,
previousblockhash: block.previousblockhash, previousblockhash: block.previousblockhash,
mediantime: block.mediantime,
}; };
} }
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> { $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing // If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId]; const txInMempool = this.mempool.getMempool()[txId];
if (txInMempool && addPrevout) { if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool); return this.$addPrevouts(txInMempool);
} }
@@ -119,7 +121,7 @@ class BitcoinApi implements AbstractBitcoinApi {
$getAddressPrefix(prefix: string): string[] { $getAddressPrefix(prefix: string): string[] {
const found: { [address: string]: string } = {}; const found: { [address: string]: string } = {};
const mp = mempool.getMempool(); const mp = this.mempool.getMempool();
for (const tx in mp) { for (const tx in mp) {
for (const vout of mp[tx].vout) { for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) { if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
@@ -261,7 +263,7 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction; return transaction;
} }
let mempoolEntry: IBitcoinApi.MempoolEntry; let mempoolEntry: IBitcoinApi.MempoolEntry;
if (!mempool.isInSync() && !this.rawMempoolCache) { if (!this.mempool.isInSync() && !this.rawMempoolCache) {
this.rawMempoolCache = await this.$getRawMempoolVerbose(); this.rawMempoolCache = await this.$getRawMempoolVerbose();
} }
if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) { if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
@@ -317,10 +319,23 @@ class BitcoinApi implements AbstractBitcoinApi {
transaction.vin[i].lazy = true; transaction.vin[i].lazy = true;
continue; continue;
} }
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false); let innerTx;
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout]; try {
this.addInnerScriptsToVin(transaction.vin[i]); innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
totalIn += innerTx.vout[transaction.vin[i].vout].value; } catch (e) {
if (this.backupBitcoinApi) {
// input tx is confirmed, but the preferred client has txindex=0, so fetch from the backup client instead.
const backupTx = await this.backupBitcoinApi.$getRawTransaction(transaction.vin[i].txid);
innerTx = JSON.parse(JSON.stringify(backupTx));
} else {
throw e;
}
}
if (innerTx) {
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
}
} }
if (lazyPrevouts && transaction.vin.length > 12) { if (lazyPrevouts && transaction.vin.length > 12) {
transaction.fee = -1; transaction.fee = -1;

View File

@@ -6,7 +6,7 @@ import websocketHandler from '../websocket-handler';
import mempool from '../mempool'; import mempool from '../mempool';
import feeApi from '../fee-api'; import feeApi from '../fee-api';
import mempoolBlocks from '../mempool-blocks'; import mempoolBlocks from '../mempool-blocks';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin-api-factory'; import bitcoinApi from './bitcoin-api-factory';
import { Common } from '../common'; import { Common } from '../common';
import backendInfo from '../backend-info'; import backendInfo from '../backend-info';
import transactionUtils from '../transaction-utils'; import transactionUtils from '../transaction-utils';
@@ -19,6 +19,7 @@ import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment'; import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository'; import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache'; import rbfCache from '../rbf-cache';
import altMempool from '../alt-mempool';
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -32,6 +33,7 @@ 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/alt/:txId', this.getAltTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', 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)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm) .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
@@ -95,8 +97,6 @@ class BitcoinRoutes {
.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)
.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/:to', this.getBlocksByBulk.bind(this))
; ;
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@@ -217,20 +217,13 @@ class BitcoinRoutes {
res.json(cpfpInfo); res.json(cpfpInfo);
return; return;
} else { } else {
let cpfpInfo; const cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
if (config.DATABASE.ENABLED) {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
}
if (cpfpInfo) { if (cpfpInfo) {
res.json(cpfpInfo); res.json(cpfpInfo);
return; return;
} else {
res.json({
ancestors: []
});
return;
} }
} }
res.status(404).send(`Transaction has no CPFP info available.`);
} }
private getBackendInfo(req: Request, res: Response) { private getBackendInfo(req: Request, res: Response) {
@@ -250,6 +243,23 @@ class BitcoinRoutes {
} }
} }
private async getAltTransaction(req: Request, res: Response) {
try {
const transaction = altMempool.getTransaction(req.params.txId);
if (transaction) {
res.json(transaction);
} else {
res.status(404).send('No such transaction in the alternate mempool');
}
} catch (e) {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getRawTransaction(req: Request, res: Response) { private async getRawTransaction(req: Request, res: Response) {
try { try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
@@ -411,41 +421,6 @@ class BitcoinRoutes {
} }
} }
private async getBlocksByBulk(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid, Bisq - Not implemented
return res.status(404).send(`This API is only available for Bitcoin networks`);
}
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
}
if (!Common.indexingEnabled()) {
return res.status(404).send(`Indexing is required for this API`);
}
const from = parseInt(req.params.from, 10);
if (!req.params.from || from < 0) {
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
}
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
if (to < 0) {
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
}
if (from > to) {
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
}
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getLegacyBlocks(req: Request, res: Response) { private async getLegacyBlocks(req: Request, res: Response) {
try { try {
const returnBlocks: IEsploraApi.Block[] = []; const returnBlocks: IEsploraApi.Block[] = [];
@@ -468,7 +443,7 @@ class BitcoinRoutes {
returnBlocks.push(localBlock); returnBlocks.push(localBlock);
nextHash = localBlock.previousblockhash; nextHash = localBlock.previousblockhash;
} else { } else {
const block = await bitcoinCoreApi.$getBlock(nextHash); const block = await bitcoinApi.$getBlock(nextHash);
returnBlocks.push(block); returnBlocks.push(block);
nextHash = block.previousblockhash; nextHash = block.previousblockhash;
} }
@@ -651,7 +626,7 @@ class BitcoinRoutes {
if (result) { if (result) {
res.json(result); res.json(result);
} else { } else {
res.status(204).send(); res.status(404).send('not found');
} }
} 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

@@ -12,8 +12,8 @@ import memoryCache from '../memory-cache';
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
private electrumClient: any; private electrumClient: any;
constructor(bitcoinClient: any) { constructor(bitcoinClient: any, mempool: any) {
super(bitcoinClient); super(bitcoinClient, mempool);
const electrumConfig = { client: 'mempool-v2', version: '1.4' }; const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null }; const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };

View File

@@ -88,7 +88,6 @@ export namespace IEsploraApi {
size: number; size: number;
weight: number; weight: number;
previousblockhash: string; previousblockhash: string;
mediantime: number;
} }
export interface Address { export interface Address {

View File

@@ -1,13 +1,8 @@
import config from '../../config'; import config from '../../config';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
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';
const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true })
});
class ElectrsApi implements AbstractBitcoinApi { class ElectrsApi implements AbstractBitcoinApi {
axiosConfig: AxiosRequestConfig = { axiosConfig: AxiosRequestConfig = {
timeout: 10000, timeout: 10000,
@@ -16,52 +11,52 @@ class ElectrsApi implements AbstractBitcoinApi {
constructor() { } constructor() { }
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> { $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig) return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> { $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig) return axios.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getBlockHeightTip(): Promise<number> { $getBlockHeightTip(): Promise<number> {
return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getBlockHashTip(): Promise<string> { $getBlockHashTip(): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig) return axios.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getTxIdsForBlock(hash: string): Promise<string[]> { $getTxIdsForBlock(hash: string): Promise<string[]> {
return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig) return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getBlockHeader(hash: string): Promise<string> { $getBlockHeader(hash: string): Promise<string> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig) return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getBlock(hash: string): Promise<IEsploraApi.Block> { $getBlock(hash: string): Promise<IEsploraApi.Block> {
return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig) return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getRawBlock(hash: string): Promise<Buffer> { $getRawBlock(hash: string): Promise<Buffer> {
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' }) return axios.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); });
} }
@@ -82,12 +77,12 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> { $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);
} }

View File

@@ -1,8 +1,8 @@
import config from '../config'; import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi 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 } from '../mempool.interfaces'; import { BlockExtended, 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';
@@ -13,9 +13,11 @@ import poolsRepository from '../repositories/PoolsRepository';
import blocksRepository from '../repositories/BlocksRepository'; import blocksRepository from '../repositories/BlocksRepository';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import BitcoinApi from './bitcoin/bitcoin-api'; import BitcoinApi from './bitcoin/bitcoin-api';
import { prepareBlock } from '../utils/blocks-utils';
import BlocksRepository from '../repositories/BlocksRepository'; import BlocksRepository from '../repositories/BlocksRepository';
import HashratesRepository from '../repositories/HashratesRepository'; import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer'; import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import poolsParser from './pools-parser'; import poolsParser from './pools-parser';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
@@ -24,7 +26,6 @@ import mining from './mining/mining';
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository'; import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
import PricesRepository from '../repositories/PricesRepository'; import PricesRepository from '../repositories/PricesRepository';
import priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import chainTips from './chain-tips';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@@ -142,11 +143,8 @@ class Blocks {
* @param block * @param block
* @returns BlockSummary * @returns BlockSummary
*/ */
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary { private summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
if (Common.isLiquid()) { const stripped = block.tx.map((tx) => {
block = this.convertLiquidFees(block);
}
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
return { return {
txid: tx.txid, txid: tx.txid,
vsize: tx.weight / 4, vsize: tx.weight / 4,
@@ -161,13 +159,6 @@ class Blocks {
}; };
} }
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
block.tx.forEach(tx => {
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
});
return block;
}
/** /**
* Return a block with additional data (reward, coinbase, fees...) * Return a block with additional data (reward, coinbase, fees...)
* @param block * @param block
@@ -175,81 +166,33 @@ class Blocks {
* @returns BlockExtended * @returns BlockExtended
*/ */
private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise<BlockExtended> { private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise<BlockExtended> {
const coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); const blockExtended: BlockExtended = Object.assign({ extras: {} }, block);
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
const blk: Partial<BlockExtended> = Object.assign({}, block); blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
const extras: Partial<BlockExtension> = {}; blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
blockExtended.extras.usd = fiatConversion.getConversionRates().USD;
extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
extras.coinbaseRaw = coinbaseTx.vin[0].scriptsig;
extras.orphans = chainTips.getOrphanedBlocksAtHeight(blk.height);
if (block.height === 0) { if (block.height === 0) {
extras.medianFee = 0; // 50th percentiles blockExtended.extras.medianFee = 0; // 50th percentiles
extras.feeRange = [0, 0, 0, 0, 0, 0, 0]; blockExtended.extras.feeRange = [0, 0, 0, 0, 0, 0, 0];
extras.totalFees = 0; blockExtended.extras.totalFees = 0;
extras.avgFee = 0; blockExtended.extras.avgFee = 0;
extras.avgFeeRate = 0; blockExtended.extras.avgFeeRate = 0;
extras.utxoSetChange = 0;
extras.avgTxSize = 0;
extras.totalInputs = 0;
extras.totalOutputs = 1;
extras.totalOutputAmt = 0;
extras.segwitTotalTxs = 0;
extras.segwitTotalSize = 0;
extras.segwitTotalWeight = 0;
} else { } else {
const stats: IBitcoinApi.BlockStats = await bitcoinClient.getBlockStats(block.id); const stats = await bitcoinClient.getBlockStats(block.id, [
extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles 'feerate_percentiles', 'minfeerate', 'maxfeerate', 'totalfee', 'avgfee', 'avgfeerate'
extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); ]);
extras.totalFees = stats.totalfee; blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
extras.avgFee = stats.avgfee; blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat();
extras.avgFeeRate = stats.avgfeerate; blockExtended.extras.totalFees = stats.totalfee;
extras.utxoSetChange = stats.utxo_increase; blockExtended.extras.avgFee = stats.avgfee;
extras.avgTxSize = Math.round(stats.total_size / stats.txs * 100) * 0.01; blockExtended.extras.avgFeeRate = stats.avgfeerate;
extras.totalInputs = stats.ins;
extras.totalOutputs = stats.outs;
extras.totalOutputAmt = stats.total_out;
extras.segwitTotalTxs = stats.swtxs;
extras.segwitTotalSize = stats.swtotal_size;
extras.segwitTotalWeight = stats.swtotal_weight;
}
if (Common.blocksSummariesIndexingEnabled()) {
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(block.id);
if (extras.feePercentiles !== null) {
extras.medianFeeAmt = extras.feePercentiles[3];
}
}
extras.virtualSize = block.weight / 4.0;
if (coinbaseTx?.vout.length > 0) {
extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null;
extras.coinbaseSignature = coinbaseTx.vout[0].scriptpubkey_asm ?? null;
extras.coinbaseSignatureAscii = transactionUtils.hex2ascii(coinbaseTx.vin[0].scriptsig) ?? null;
} else {
extras.coinbaseAddress = null;
extras.coinbaseSignature = null;
extras.coinbaseSignatureAscii = null;
}
const header = await bitcoinClient.getBlockHeader(block.id, false);
extras.header = header;
const coinStatsIndex = indexer.isCoreIndexReady('coinstatsindex');
if (coinStatsIndex !== null && coinStatsIndex.best_block_height >= block.height) {
const txoutset = await bitcoinClient.getTxoutSetinfo('none', block.height);
extras.utxoSetSize = txoutset.txouts,
extras.totalInputAmt = Math.round(txoutset.block_info.prevout_spent * 100000000);
} else {
extras.utxoSetSize = null;
extras.totalInputAmt = null;
} }
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag; let pool: PoolTag;
if (coinbaseTx !== undefined) { if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(coinbaseTx); pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
} else { } else {
if (config.DATABASE.ENABLED === true) { if (config.DATABASE.ENABLED === true) {
pool = await poolsRepository.$getUnknownPool(); pool = await poolsRepository.$getUnknownPool();
@@ -259,27 +202,23 @@ class Blocks {
} }
if (!pool) { // We should never have this situation in practise if (!pool) { // We should never have this situation in practise
logger.warn(`Cannot assign pool to block ${blk.height} and 'unknown' pool does not exist. ` + logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
`Check your "pools" table entries`); `Check your "pools" table entries`);
} else { } else {
extras.pool = { blockExtended.extras.pool = {
id: pool.uniqueId, id: pool.id,
name: pool.name, name: pool.name,
slug: pool.slug, slug: pool.slug,
}; };
} }
extras.matchRate = null; const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
if (config.MEMPOOL.AUDIT) { if (auditScore != null) {
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id); blockExtended.extras.matchRate = auditScore.matchRate;
if (auditScore != null) {
extras.matchRate = auditScore.matchRate;
}
} }
} }
blk.extras = <BlockExtension>extras; return blockExtended;
return <BlockExtended>blk;
} }
/** /**
@@ -305,18 +244,15 @@ class Blocks {
} else { } else {
pools = poolsParser.miningPools; pools = poolsParser.miningPools;
} }
for (let i = 0; i < pools.length; ++i) { for (let i = 0; i < pools.length; ++i) {
if (address !== undefined) { if (address !== undefined) {
const addresses: string[] = typeof pools[i].addresses === 'string' ? const addresses: string[] = JSON.parse(pools[i].addresses);
JSON.parse(pools[i].addresses) : pools[i].addresses;
if (addresses.indexOf(address) !== -1) { if (addresses.indexOf(address) !== -1) {
return pools[i]; return pools[i];
} }
} }
const regexes: string[] = typeof pools[i].regexes === 'string' ? const regexes: string[] = JSON.parse(pools[i].regexes);
JSON.parse(pools[i].regexes) : pools[i].regexes;
for (let y = 0; y < regexes.length; ++y) { for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i'); const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex); const match = asciiScriptSig.match(regex);
@@ -494,7 +430,7 @@ class Blocks {
loadingIndicators.setProgress('block-indexing', progress, false); loadingIndicators.setProgress('block-indexing', progress, false);
} }
const blockHash = await bitcoinApi.$getBlockHash(blockHeight); const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash); const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -542,13 +478,13 @@ class Blocks {
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);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash); const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
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);
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash); const previousPeriodBlock = await bitcoinClient.getBlock(previousPeriodBlockHash)
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.`);
} }
@@ -563,7 +499,6 @@ class Blocks {
} else { } else {
this.currentBlockHeight++; this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`); logger.debug(`New block found (#${this.currentBlockHeight})!`);
await chainTips.updateOrphanedBlocks();
} }
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight); const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
@@ -580,18 +515,18 @@ class Blocks {
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);
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) { if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock['hash']) {
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`); 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
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 BlocksSummariesRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10); await cpfpRepository.$deleteClustersFrom(lastBlock['height'] - 10);
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);
await this.$getStrippedBlockTransactions(newBlock.id, true, true); await this.$getStrippedBlockTransactions(newBlock.id, true, true);
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);
} }
} }
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
@@ -651,7 +586,7 @@ 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 % 6 === 0)) { if (!memPool.hasPriority()) {
diskCache.$saveCacheToDisk(); diskCache.$saveCacheToDisk();
} }
@@ -664,27 +599,23 @@ class Blocks {
* Index a block if it's missing from the database. Returns the block after indexing * Index a block if it's missing from the database. Returns the block after indexing
*/ */
public async $indexBlock(height: number): Promise<BlockExtended> { public async $indexBlock(height: number): Promise<BlockExtended> {
if (Common.indexingEnabled()) { const dbBlock = await blocksRepository.$getBlockByHeight(height);
const dbBlock = await blocksRepository.$getBlockByHeight(height); if (dbBlock != null) {
if (dbBlock !== null) { return prepareBlock(dbBlock);
return dbBlock;
}
} }
const blockHash = await bitcoinApi.$getBlockHash(height); const blockHash = await bitcoinApi.$getBlockHash(height);
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash); const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true); const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) { await blocksRepository.$saveBlockInDatabase(blockExtended);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
return blockExtended; return prepareBlock(blockExtended);
} }
/** /**
* Get one block by its hash * Index a block by hash if it's missing from the database. Returns the block after indexing
*/ */
public async $getBlock(hash: string): Promise<BlockExtended | IEsploraApi.Block> { public async $getBlock(hash: string): Promise<BlockExtended | IEsploraApi.Block> {
// Check the memory cache // Check the memory cache
@@ -693,14 +624,31 @@ class Blocks {
return blockByHash; return blockByHash;
} }
// Not Bitcoin network, return the block as it from the bitcoin backend // Block has already been indexed
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { if (Common.indexingEnabled()) {
return await bitcoinCoreApi.$getBlock(hash); const dbBlock = await blocksRepository.$getBlockByHash(hash);
if (dbBlock != null) {
return prepareBlock(dbBlock);
}
} }
// Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return await bitcoinApi.$getBlock(hash);
}
let block = await bitcoinClient.getBlock(hash);
block = prepareBlock(block);
// Bitcoin network, add our custom data on top // Bitcoin network, add our custom data on top
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(hash); const transactions = await this.$getTransactionsExtended(hash, block.height, true);
return await this.$indexBlock(block.height); const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) {
delete(blockExtended['coinbaseTx']);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
return blockExtended;
} }
public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false, public async $getStrippedBlockTransactions(hash: string, skipMemoryCache = false,
@@ -734,19 +682,8 @@ class Blocks {
return summary.transactions; return summary.transactions;
} }
/**
* Get 15 blocks
*
* Internally this function uses two methods to get the blocks, and
* the method is automatically selected:
* - Using previous block hash links
* - Using block height
*
* @param fromHeight
* @param limit
* @returns
*/
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight; let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight;
if (currentHeight > this.currentBlockHeight) { if (currentHeight > this.currentBlockHeight) {
limit -= currentHeight - this.currentBlockHeight; limit -= currentHeight - this.currentBlockHeight;
@@ -758,15 +695,27 @@ class Blocks {
return returnBlocks; return returnBlocks;
} }
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) { for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight); let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) { if (block) {
// Using the memory cache (find by height)
returnBlocks.push(block); returnBlocks.push(block);
} else { } else if (Common.indexingEnabled()) {
// Using indexing (find by height, index on the fly, save in database)
block = await this.$indexBlock(currentHeight); block = await this.$indexBlock(currentHeight);
returnBlocks.push(block); returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
} }
currentHeight--; currentHeight--;
} }
@@ -774,114 +723,6 @@ class Blocks {
return returnBlocks; return returnBlocks;
} }
/**
* Used for bulk block data query
*
* @param fromHeight
* @param toHeight
*/
public async $getBlocksBetweenHeight(fromHeight: number, toHeight: number): Promise<any> {
if (!Common.indexingEnabled()) {
return [];
}
const blocks: any[] = [];
while (fromHeight <= toHeight) {
let block: BlockExtended | null = await blocksRepository.$getBlockByHeight(fromHeight);
if (!block) {
await this.$indexBlock(fromHeight);
block = await blocksRepository.$getBlockByHeight(fromHeight);
if (!block) {
continue;
}
}
// Cleanup fields before sending the response
const cleanBlock: any = {
height: block.height ?? null,
hash: block.id ?? null,
timestamp: block.timestamp ?? null,
median_timestamp: block.mediantime ?? null,
previous_block_hash: block.previousblockhash ?? null,
difficulty: block.difficulty ?? null,
header: block.extras.header ?? null,
version: block.version ?? null,
bits: block.bits ?? null,
nonce: block.nonce ?? null,
size: block.size ?? null,
weight: block.weight ?? null,
tx_count: block.tx_count ?? null,
merkle_root: block.merkle_root ?? null,
reward: block.extras.reward ?? null,
total_fee_amt: block.extras.totalFees ?? null,
avg_fee_amt: block.extras.avgFee ?? null,
median_fee_amt: block.extras.medianFeeAmt ?? null,
fee_amt_percentiles: block.extras.feePercentiles ?? null,
avg_fee_rate: block.extras.avgFeeRate ?? null,
median_fee_rate: block.extras.medianFee ?? null,
fee_rate_percentiles: block.extras.feeRange ?? null,
total_inputs: block.extras.totalInputs ?? null,
total_input_amt: block.extras.totalInputAmt ?? null,
total_outputs: block.extras.totalOutputs ?? null,
total_output_amt: block.extras.totalOutputAmt ?? null,
segwit_total_txs: block.extras.segwitTotalTxs ?? null,
segwit_total_size: block.extras.segwitTotalSize ?? null,
segwit_total_weight: block.extras.segwitTotalWeight ?? null,
avg_tx_size: block.extras.avgTxSize ?? null,
utxoset_change: block.extras.utxoSetChange ?? null,
utxoset_size: block.extras.utxoSetSize ?? null,
coinbase_raw: block.extras.coinbaseRaw ?? null,
coinbase_address: block.extras.coinbaseAddress ?? null,
coinbase_signature: block.extras.coinbaseSignature ?? null,
coinbase_signature_ascii: block.extras.coinbaseSignatureAscii ?? null,
pool_slug: block.extras.pool.slug ?? null,
pool_id: block.extras.pool.id ?? null,
};
if (Common.blocksSummariesIndexingEnabled() && cleanBlock.fee_amt_percentiles === null) {
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
if (cleanBlock.fee_amt_percentiles === null) {
const block = await bitcoinClient.getBlock(cleanBlock.hash, 2);
const summary = this.summarizeBlock(block);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
cleanBlock.fee_amt_percentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(cleanBlock.hash);
}
if (cleanBlock.fee_amt_percentiles !== null) {
cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3];
}
}
cleanBlock.fee_amt_percentiles = {
'min': cleanBlock.fee_amt_percentiles[0],
'perc_10': cleanBlock.fee_amt_percentiles[1],
'perc_25': cleanBlock.fee_amt_percentiles[2],
'perc_50': cleanBlock.fee_amt_percentiles[3],
'perc_75': cleanBlock.fee_amt_percentiles[4],
'perc_90': cleanBlock.fee_amt_percentiles[5],
'max': cleanBlock.fee_amt_percentiles[6],
};
cleanBlock.fee_rate_percentiles = {
'min': cleanBlock.fee_rate_percentiles[0],
'perc_10': cleanBlock.fee_rate_percentiles[1],
'perc_25': cleanBlock.fee_rate_percentiles[2],
'perc_50': cleanBlock.fee_rate_percentiles[3],
'perc_75': cleanBlock.fee_rate_percentiles[4],
'perc_90': cleanBlock.fee_rate_percentiles[5],
'max': cleanBlock.fee_rate_percentiles[6],
};
// Re-org can happen after indexing so we need to always get the
// latest state from core
cleanBlock.orphans = chainTips.getOrphanedBlocksAtHeight(cleanBlock.height);
blocks.push(cleanBlock);
fromHeight++;
}
return blocks;
}
public async $getBlockAuditSummary(hash: string): Promise<any> { public async $getBlockAuditSummary(hash: string): Promise<any> {
let summary; let summary;
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {

View File

@@ -1,61 +0,0 @@
import logger from '../logger';
import bitcoinClient from './bitcoin/bitcoin-client';
export interface ChainTip {
height: number;
hash: string;
branchlen: number;
status: 'invalid' | 'active' | 'valid-fork' | 'valid-headers' | 'headers-only';
};
export interface OrphanedBlock {
height: number;
hash: string;
status: 'valid-fork' | 'valid-headers' | 'headers-only';
}
class ChainTips {
private chainTips: ChainTip[] = [];
private orphanedBlocks: OrphanedBlock[] = [];
public async updateOrphanedBlocks(): Promise<void> {
try {
this.chainTips = await bitcoinClient.getChainTips();
this.orphanedBlocks = [];
for (const chain of this.chainTips) {
if (chain.status === 'valid-fork' || chain.status === 'valid-headers') {
let block = await bitcoinClient.getBlock(chain.hash);
while (block && block.confirmations === -1) {
this.orphanedBlocks.push({
height: block.height,
hash: block.hash,
status: chain.status
});
block = await bitcoinClient.getBlock(block.previousblockhash);
}
}
}
logger.debug(`Updated orphaned blocks cache. Found ${this.orphanedBlocks.length} orphaned blocks`);
} catch (e) {
logger.err(`Cannot get fetch orphaned blocks. Reason: ${e instanceof Error ? e.message : e}`);
}
}
public getOrphanedBlocksAtHeight(height: number | undefined): OrphanedBlock[] {
if (height === undefined) {
return [];
}
const orphans: OrphanedBlock[] = [];
for (const block of this.orphanedBlocks) {
if (block.height === height) {
orphans.push(block);
}
}
return orphans;
}
}
export default new ChainTips();

View File

@@ -1,4 +1,4 @@
import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import { CpfpInfo, 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';
@@ -35,25 +35,16 @@ export class Common {
} }
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) { static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
const filtered: TransactionExtended[] = []; const arr = [transactions[transactions.length - 1].effectiveFeePerVsize];
let lastValidRate = Infinity;
// filter out anomalous fee rates to ensure monotonic range
for (const tx of transactions) {
if (tx.effectiveFeePerVsize <= lastValidRate) {
filtered.push(tx);
lastValidRate = tx.effectiveFeePerVsize;
}
}
const arr = [filtered[filtered.length - 1].effectiveFeePerVsize];
const chunk = 1 / (rangeLength - 1); const chunk = 1 / (rangeLength - 1);
let itemsToAdd = rangeLength - 2; let itemsToAdd = rangeLength - 2;
while (itemsToAdd > 0) { while (itemsToAdd > 0) {
arr.push(filtered[Math.floor(filtered.length * chunk * itemsToAdd)].effectiveFeePerVsize); arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].effectiveFeePerVsize);
itemsToAdd--; itemsToAdd--;
} }
arr.push(filtered[0].effectiveFeePerVsize); arr.push(transactions[0].effectiveFeePerVsize);
return arr; return arr;
} }
@@ -164,30 +155,6 @@ export class Common {
return parents; return parents;
} }
// calculates the ratio of matched transactions to projected transactions by weight
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
let matchedWeight = 0;
let projectedWeight = 0;
const inBlock = {};
for (const tx of transactions) {
inBlock[tx.txid] = tx;
}
// look for transactions that were expected in the template, but missing from the mined block
for (const tx of projectedBlock.transactions) {
if (inBlock[tx.txid]) {
matchedWeight += tx.vsize * 4;
}
projectedWeight += tx.vsize * 4;
}
projectedWeight += transactions[0].weight;
matchedWeight += transactions[0].weight;
return projectedWeight ? matchedWeight / projectedWeight : 1;
}
static getSqlInterval(interval: string | null): string | null { static getSqlInterval(interval: string | null): string | null {
switch (interval) { switch (interval) {
case '24h': return '1 DAY'; case '24h': return '1 DAY';
@@ -199,7 +166,6 @@ export class Common {
case '1y': return '1 YEAR'; case '1y': return '1 YEAR';
case '2y': return '2 YEAR'; case '2y': return '2 YEAR';
case '3y': return '3 YEAR'; case '3y': return '3 YEAR';
case '4y': return '4 YEAR';
default: return null; default: return null;
} }
} }
@@ -262,21 +228,14 @@ export class Common {
].join('x'); ].join('x');
} }
static utcDateToMysql(date?: number | null): string | null { static utcDateToMysql(date?: number): string {
if (date === null) {
return null;
}
const d = new Date((date || 0) * 1000); const d = new Date((date || 0) * 1000);
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0]; return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
} }
static findSocketNetwork(addr: string): {network: string | null, url: string} { static findSocketNetwork(addr: string): {network: string | null, url: string} {
let network: string | null = null; let network: string | null = null;
let url: string = addr; let url = addr.split('://')[1];
if (config.LIGHTNING.BACKEND === 'cln') {
url = addr.split('://')[1];
}
if (!url) { if (!url) {
return { return {
@@ -293,7 +252,7 @@ export class Common {
} }
} else if (addr.indexOf('i2p') !== -1) { } else if (addr.indexOf('i2p') !== -1) {
network = 'i2p'; network = 'i2p';
} else if (addr.indexOf('ipv4') !== -1 || (config.LIGHTNING.BACKEND === 'lnd' && isIP(url.split(':')[0]) === 4)) { } else if (addr.indexOf('ipv4') !== -1) {
const ipv = isIP(url.split(':')[0]); const ipv = isIP(url.split(':')[0]);
if (ipv === 4) { if (ipv === 4) {
network = 'ipv4'; network = 'ipv4';
@@ -303,7 +262,7 @@ export class Common {
url: addr, url: addr,
}; };
} }
} else if (addr.indexOf('ipv6') !== -1 || (config.LIGHTNING.BACKEND === 'lnd' && url.indexOf(']:'))) { } else if (addr.indexOf('ipv6') !== -1) {
url = url.split('[')[1].split(']')[0]; url = url.split('[')[1].split(']')[0];
const ipv = isIP(url); const ipv = isIP(url);
if (ipv === 6) { if (ipv === 6) {

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 59; private static currentVersion = 52;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@@ -62,8 +62,8 @@ class DatabaseMigration {
if (databaseSchemaVersion <= 2) { if (databaseSchemaVersion <= 2) {
// Disable some spam logs when they're not relevant // Disable some spam logs when they're not relevant
this.uniqueLog(logger.notice, this.blocksTruncatedMessage); this.uniqueLogs.push(this.blocksTruncatedMessage);
this.uniqueLog(logger.notice, this.hashratesTruncatedMessage); this.uniqueLogs.push(this.hashratesTruncatedMessage);
} }
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion); logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
@@ -86,7 +86,7 @@ class DatabaseMigration {
try { try {
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion); await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
if (databaseSchemaVersion === 0) { if (databaseSchemaVersion === 0) {
logger.notice(`MIGRATIONS: OK. Database schema has been properly initialized to version ${DatabaseMigration.currentVersion} (latest version)`); logger.notice(`MIGRATIONS: OK. Database schema has been properly initialized to version ${DatabaseMigration.currentVersion} (latest version)`);
} else { } else {
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`); logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
} }
@@ -300,7 +300,7 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.updateToSchemaVersion(27); await this.updateToSchemaVersion(27);
} }
if (databaseSchemaVersion < 28 && isBitcoin === true) { if (databaseSchemaVersion < 28 && isBitcoin === true) {
if (config.LIGHTNING.ENABLED) { if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`); this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`);
@@ -461,60 +461,13 @@ class DatabaseMigration {
await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions')); await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions'));
try { try {
await this.$convertCompactCpfpTables(); await this.$convertCompactCpfpTables();
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`'); await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
await this.updateToSchemaVersion(52); await this.updateToSchemaVersion(52);
} catch (e) { } catch(e) {
logger.warn('' + (e instanceof Error ? e.message : e)); logger.warn('' + (e instanceof Error ? e.message : e));
} }
} }
if (databaseSchemaVersion < 53) {
await this.$executeQuery('ALTER TABLE statistics MODIFY mempool_byte_weight bigint(20) UNSIGNED NOT NULL');
await this.updateToSchemaVersion(53);
}
if (databaseSchemaVersion < 54) {
this.uniqueLog(logger.notice, `'prices' table has been truncated`);
await this.$executeQuery(`TRUNCATE prices`);
if (isBitcoin === true) {
this.uniqueLog(logger.notice, `'blocks_prices' table has been truncated`);
await this.$executeQuery(`TRUNCATE blocks_prices`);
}
await this.updateToSchemaVersion(54);
}
if (databaseSchemaVersion < 55) {
await this.$executeQuery(this.getAdditionalBlocksDataQuery());
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.updateToSchemaVersion(55);
}
if (databaseSchemaVersion < 56) {
await this.$executeQuery('ALTER TABLE pools ADD unique_id int NOT NULL DEFAULT -1');
await this.$executeQuery('TRUNCATE TABLE `blocks`');
this.uniqueLog(logger.notice, this.blocksTruncatedMessage);
await this.$executeQuery('DELETE FROM `pools`');
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
this.uniqueLog(logger.notice, '`pools` table has been truncated`');
await this.updateToSchemaVersion(56);
}
if (databaseSchemaVersion < 57 && isBitcoin === true) {
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
await this.updateToSchemaVersion(57);
}
if (databaseSchemaVersion < 58) {
// We only run some migration queries for this version
await this.updateToSchemaVersion(58);
}
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
// https://github.com/mempool/mempool/issues/3360
await this.$executeQuery(`TRUNCATE prices`);
}
} }
/** /**
@@ -638,15 +591,10 @@ class DatabaseMigration {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`); queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
} }
if (version < 9 && isBitcoin === true) { if (version < 9 && isBitcoin === true) {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`); queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`);
} }
if (version < 58) {
queries.push(`DELETE FROM state WHERE name = 'last_hashrates_indexing'`);
queries.push(`DELETE FROM state WHERE name = 'last_weekly_hashrates_indexing'`);
}
return queries; return queries;
} }
@@ -793,28 +741,6 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
private getAdditionalBlocksDataQuery(): string {
return `ALTER TABLE blocks
ADD median_timestamp timestamp NOT NULL,
ADD coinbase_address varchar(100) NULL,
ADD coinbase_signature varchar(500) NULL,
ADD coinbase_signature_ascii varchar(500) NULL,
ADD avg_tx_size double unsigned NOT NULL,
ADD total_inputs int unsigned NOT NULL,
ADD total_outputs int unsigned NOT NULL,
ADD total_output_amt bigint unsigned NOT NULL,
ADD fee_percentiles longtext NULL,
ADD median_fee_amt int unsigned NULL,
ADD segwit_total_txs int unsigned NOT NULL,
ADD segwit_total_size int unsigned NOT NULL,
ADD segwit_total_weight int unsigned NOT NULL,
ADD header varchar(160) NOT NULL,
ADD utxoset_change int NOT NULL,
ADD utxoset_size int unsigned NULL,
ADD total_input_amt bigint unsigned NULL
`;
}
private getCreateDailyStatsTableQuery(): string { private getCreateDailyStatsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS hashrates ( return `CREATE TABLE IF NOT EXISTS hashrates (
hashrate_timestamp timestamp NOT NULL, hashrate_timestamp timestamp NOT NULL,
@@ -1032,16 +958,25 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
public async $blocksReindexingTruncate(): Promise<void> { public async $truncateIndexedData(tables: string[]) {
logger.warn(`Truncating pools, blocks and hashrates for re-indexing (using '--reindex-blocks'). You can cancel this command within 5 seconds`); const allowedTables = ['blocks', 'hashrates', 'prices'];
await Common.sleep$(5000);
await this.$executeQuery(`TRUNCATE blocks`); try {
await this.$executeQuery(`TRUNCATE hashrates`); for (const table of tables) {
await this.$executeQuery(`TRUNCATE difficulty_adjustments`); if (!allowedTables.includes(table)) {
await this.$executeQuery('DELETE FROM `pools`'); logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`);
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1'); continue;
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`); }
await this.$executeQuery(`TRUNCATE ${table}`, true);
if (table === 'hashrates') {
await this.$executeQuery('UPDATE state set number = 0 where name = "last_hashrates_indexing"', true);
}
logger.notice(`Table ${table} has been truncated`);
}
} catch (e) {
logger.warn(`Unable to erase indexed data`);
}
} }
private async $convertCompactCpfpTables(): Promise<void> { private async $convertCompactCpfpTables(): Promise<void> {

View File

@@ -9,11 +9,9 @@ export interface DifficultyAdjustment {
remainingBlocks: number; // Block count remainingBlocks: number; // Block count
remainingTime: number; // Duration of time in ms remainingTime: number; // Duration of time in ms
previousRetarget: number; // Percent: -75 to 300 previousRetarget: number; // Percent: -75 to 300
previousTime: number; // Unix time in ms
nextRetargetHeight: number; // Block Height nextRetargetHeight: number; // Block Height
timeAvg: number; // Duration of time in ms timeAvg: number; // Duration of time in ms
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
expectedBlocks: number; // Block count
} }
export function calcDifficultyAdjustment( export function calcDifficultyAdjustment(
@@ -34,12 +32,12 @@ export function calcDifficultyAdjustment(
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;
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0; const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
let difficultyChange = 0; let difficultyChange = 0;
let timeAvgSecs = diffSeconds / blocksInEpoch; let timeAvgSecs = BLOCK_SECONDS_TARGET;
// Only calculate the estimate once we have 7.2% of blocks in current epoch // Only calculate the estimate once we have 7.2% of blocks in current epoch
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) { if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
timeAvgSecs = diffSeconds / blocksInEpoch;
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100; difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
// Max increase is x4 (+300%) // Max increase is x4 (+300%)
if (difficultyChange > 300) { if (difficultyChange > 300) {
@@ -76,11 +74,9 @@ export function calcDifficultyAdjustment(
remainingBlocks, remainingBlocks,
remainingTime, remainingTime,
previousRetarget, previousRetarget,
previousTime: DATime,
nextRetargetHeight, nextRetargetHeight,
timeAvg, timeAvg,
timeOffset, timeOffset,
expectedBlocks,
}; };
} }

View File

@@ -9,35 +9,21 @@ import { TransactionExtended } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
class DiskCache { class DiskCache {
private cacheSchemaVersion = 3; private cacheSchemaVersion = 1;
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 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 CHUNK_FILES = 25; private static CHUNK_FILES = 25;
private isWritingCache = false; private isWritingCache = false;
constructor() { constructor() { }
if (!cluster.isMaster) {
return;
}
process.on('SIGINT', (e) => {
this.saveCacheToDiskSync();
process.exit(2);
});
process.on('SIGTERM', (e) => {
this.saveCacheToDiskSync();
process.exit(2);
});
}
async $saveCacheToDisk(): Promise<void> { async $saveCacheToDisk(): Promise<void> {
if (!cluster.isPrimary) { if (!cluster.isPrimary) {
return; return;
} }
if (this.isWritingCache) { if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.'); logger.debug('Saving cache already in progress. Skipping.')
return; return;
} }
try { try {
@@ -55,7 +41,6 @@ class DiskCache {
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES); const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({ await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
network: config.MEMPOOL.NETWORK,
cacheSchemaVersion: this.cacheSchemaVersion, cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(), blocks: blocks.getBlocks(),
blockSummaries: blocks.getBlockSummaries(), blockSummaries: blocks.getBlockSummaries(),
@@ -76,79 +61,14 @@ class DiskCache {
} }
} }
saveCacheToDiskSync(): void { wipeCache() {
if (!cluster.isPrimary) { fs.unlinkSync(DiskCache.FILE_NAME);
return;
}
if (this.isWritingCache) {
logger.debug('Saving cache already in progress. Skipping.');
return;
}
try {
logger.debug('Writing mempool and blocks data to disk cache (sync)...');
this.isWritingCache = true;
const mempool = memPool.getMempool();
const mempoolArray: TransactionExtended[] = [];
for (const tx in mempool) {
mempoolArray.push(mempool[tx]);
}
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;
} catch (e) {
logger.warn('Error writing to cache file: ' + (e instanceof Error ? e.message : e));
this.isWritingCache = false;
}
}
wipeCache(): void {
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
try {
fs.unlinkSync(DiskCache.FILE_NAME);
} catch (e: any) {
if (e?.code !== 'ENOENT') {
logger.err(`Cannot wipe cache file ${DiskCache.FILE_NAME}. Exception ${JSON.stringify(e)}`);
}
}
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) { for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
const filename = DiskCache.FILE_NAMES.replace('{number}', i.toString()); fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
try {
fs.unlinkSync(filename);
} catch (e: any) {
if (e?.code !== 'ENOENT') {
logger.err(`Cannot wipe cache file ${filename}. Exception ${JSON.stringify(e)}`);
}
}
} }
} }
loadMempoolCache(): void { loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) { if (!fs.existsSync(DiskCache.FILE_NAME)) {
return; return;
} }
@@ -162,10 +82,6 @@ class DiskCache {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.'); logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache(); return this.wipeCache();
} }
if (data.network && data.network !== config.MEMPOOL.NETWORK) {
logger.notice('Disk cache contains data from a different network. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) { if (data.mempoolArray) {
for (const tx of data.mempoolArray) { for (const tx of data.mempoolArray) {

View File

@@ -559,17 +559,6 @@ class ChannelsApi {
const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {}; const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {};
const policy2: Partial<ILightningApi.RoutingPolicy> = channel.node2_policy || {}; const policy2: Partial<ILightningApi.RoutingPolicy> = channel.node2_policy || {};
// https://github.com/mempool/mempool/issues/3006
if ((channel.last_update ?? 0) < 1514736061) { // January 1st 2018
channel.last_update = null;
}
if ((policy1.last_update ?? 0) < 1514736061) { // January 1st 2018
policy1.last_update = null;
}
if ((policy2.last_update ?? 0) < 1514736061) { // January 1st 2018
policy2.last_update = null;
}
const query = `INSERT INTO channels const query = `INSERT INTO channels
( (
id, id,

View File

@@ -228,7 +228,7 @@ class NodesApi {
nodes.capacity nodes.capacity
FROM nodes FROM nodes
ORDER BY capacity DESC ORDER BY capacity DESC
LIMIT 6 LIMIT 100
`; `;
[rows] = await DB.query(query); [rows] = await DB.query(query);
@@ -269,26 +269,14 @@ class NodesApi {
let query: string; let query: string;
if (full === false) { if (full === false) {
query = ` query = `
SELECT SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
nodes.public_key as publicKey, nodes.channels
IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
nodes.channels,
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM nodes FROM nodes
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
ORDER BY channels DESC ORDER BY channels DESC
LIMIT 6; LIMIT 100;
`; `;
[rows] = await DB.query(query); [rows] = await DB.query(query);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
}
} else { } else {
query = ` query = `
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
@@ -374,13 +362,7 @@ class NodesApi {
public async $searchNodeByPublicKeyOrAlias(search: string) { public async $searchNodeByPublicKeyOrAlias(search: string) {
try { try {
const publicKeySearch = search.replace('%', '') + '%'; const publicKeySearch = search.replace('%', '') + '%';
const aliasSearch = search const aliasSearch = search.replace(/[-_.]/g, ' ').replace(/[^a-zA-Z0-9 ]/g, '').split(' ').map((search) => '+' + search + '*').join(' ');
.replace(/[-_.]/g, ' ') // Replace all -_. characters with empty space. Eg: "ln.nicehash" becomes "ln nicehash".
.replace(/[^a-zA-Z0-9 ]/g, '') // Remove all special characters and keep just A to Z, 0 to 9.
.split(' ')
.filter(key => key.length)
.map((search) => '+' + search + '*').join(' ');
// %keyword% is wildcard search and can't be indexed so it's slower as the node database grow. keyword% can be indexed but then you can't search for "Nicehash" and get result for ln.nicehash.com. So we use fulltext index for words "ln, nicehash, com" and nicehash* will find it instantly.
const query = `SELECT public_key, alias, capacity, channels, status FROM nodes WHERE public_key LIKE ? OR MATCH alias_search AGAINST (? IN BOOLEAN MODE) ORDER BY capacity DESC LIMIT 10`; const query = `SELECT public_key, alias, capacity, channels, status FROM nodes WHERE public_key LIKE ? OR MATCH alias_search AGAINST (? IN BOOLEAN MODE) ORDER BY capacity DESC LIMIT 10`;
const [rows]: any = await DB.query(query, [publicKeySearch, aliasSearch]); const [rows]: any = await DB.query(query, [publicKeySearch, aliasSearch]);
return rows; return rows;
@@ -417,24 +399,24 @@ class NodesApi {
if (!ispList[isp1]) { if (!ispList[isp1]) {
ispList[isp1] = { ispList[isp1] = {
ids: [channel.isp1ID], id: channel.isp1ID.toString(),
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp1].ids.includes(channel.isp1ID) === false) { } else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
ispList[isp1].ids.push(channel.isp1ID); ispList[isp1].id += ',' + channel.isp1ID.toString();
} }
if (!ispList[isp2]) { if (!ispList[isp2]) {
ispList[isp2] = { ispList[isp2] = {
ids: [channel.isp2ID], id: channel.isp2ID.toString(),
capacity: 0, capacity: 0,
channels: 0, channels: 0,
nodes: {}, nodes: {},
}; };
} else if (ispList[isp2].ids.includes(channel.isp2ID) === false) { } else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
ispList[isp2].ids.push(channel.isp2ID); ispList[isp2].id += ',' + channel.isp2ID.toString();
} }
ispList[isp1].capacity += channel.capacity; ispList[isp1].capacity += channel.capacity;
@@ -444,11 +426,11 @@ class NodesApi {
ispList[isp2].channels += 1; ispList[isp2].channels += 1;
ispList[isp2].nodes[channel.node2PublicKey] = true; ispList[isp2].nodes[channel.node2PublicKey] = true;
} }
const ispRanking: any[] = []; const ispRanking: any[] = [];
for (const isp of Object.keys(ispList)) { for (const isp of Object.keys(ispList)) {
ispRanking.push([ ispRanking.push([
ispList[isp].ids.sort((a, b) => a - b).join(','), ispList[isp].id,
isp, isp,
ispList[isp].capacity, ispList[isp].capacity,
ispList[isp].channels, ispList[isp].channels,
@@ -642,11 +624,6 @@ class NodesApi {
*/ */
public async $saveNode(node: ILightningApi.Node): Promise<void> { public async $saveNode(node: ILightningApi.Node): Promise<void> {
try { try {
// https://github.com/mempool/mempool/issues/3006
if ((node.last_update ?? 0) < 1514736061) { // January 1st 2018
node.last_update = null;
}
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
const query = `INSERT INTO nodes( const query = `INSERT INTO nodes(
public_key, public_key,

View File

@@ -0,0 +1,123 @@
import logger from '../logger';
import * as http from 'http';
import * as https from 'https';
import axios, { AxiosResponse } from 'axios';
import { IConversionRates } from '../mempool.interfaces';
import config from '../config';
import backendInfo from './backend-info';
import { SocksProxyAgent } from 'socks-proxy-agent';
class FiatConversion {
private debasingFiatCurrencies = ['AED', 'AUD', 'BDT', 'BHD', 'BMD', 'BRL', 'CAD', 'CHF', 'CLP',
'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'KWD',
'LKR', 'MMK', 'MXN', 'MYR', 'NGN', 'NOK', 'NZD', 'PHP', 'PKR', 'PLN', 'RUB', 'SAR', 'SEK',
'SGD', 'THB', 'TRY', 'TWD', 'UAH', 'USD', 'VND', 'ZAR'];
private conversionRates: IConversionRates = {};
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
public ratesInitialized = false; // If true, it means rates are ready for use
constructor() {
for (const fiat of this.debasingFiatCurrencies) {
this.conversionRates[fiat] = 0;
}
}
public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
this.ratesChangedCallback = fn;
}
public startService() {
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
logger.info('Starting currency rates service');
if (config.SOCKS5PROXY.ENABLED) {
logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
} else {
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
}
setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
this.updateCurrency();
}
public getConversionRates() {
return this.conversionRates;
}
private async updateCurrency(): Promise<void> {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpAgent?: http.Agent;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
let socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
// Handle proxy agent for onion addresses
if (isHTTP) {
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
} else {
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
}
logger.debug('Querying currency rates service...');
const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
if (response.statusText === 'error' || !response.data) {
throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
}
for (const rate of response.data.data) {
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
}
}
this.ratesInitialized = true;
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);
}
break;
} catch (e) {
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
retry++;
}
}
}
}
export default new FiatConversion();

View File

@@ -55,12 +55,11 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
clChannelsDict[clChannel.short_channel_id] = clChannel; clChannelsDict[clChannel.short_channel_id] = clChannel;
clChannelsDictCount[clChannel.short_channel_id] = 1; clChannelsDictCount[clChannel.short_channel_id] = 1;
} else { } else {
const fullChannel = await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]); consolidatedChannelList.push(
if (fullChannel !== null) { await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
consolidatedChannelList.push(fullChannel); );
delete clChannelsDict[clChannel.short_channel_id]; delete clChannelsDict[clChannel.short_channel_id];
clChannelsDictCount[clChannel.short_channel_id]++; clChannelsDictCount[clChannel.short_channel_id]++;
}
} }
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
@@ -75,10 +74,7 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
channelProcessed = 0; channelProcessed = 0;
const keys = Object.keys(clChannelsDict); const keys = Object.keys(clChannelsDict);
for (const short_channel_id of keys) { for (const short_channel_id of keys) {
const incompleteChannel = await buildIncompleteChannel(clChannelsDict[short_channel_id]); consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
if (incompleteChannel !== null) {
consolidatedChannelList.push(incompleteChannel);
}
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) { if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
@@ -96,13 +92,10 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format * Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy for both nodes * In this case, clightning knows the channel policy for both nodes
*/ */
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel | null> { async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id); const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
if (!tx) {
return null;
}
const parts = clChannelA.short_channel_id.split('x'); const parts = clChannelA.short_channel_id.split('x');
const outputIdx = parts[2]; const outputIdx = parts[2];
@@ -122,11 +115,8 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format * Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
* In this case, clightning knows the channel policy of only one node * In this case, clightning knows the channel policy of only one node
*/ */
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel | null> { async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id); const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
if (!tx) {
return null;
}
const parts = clChannel.short_channel_id.split('x'); const parts = clChannel.short_channel_id.split('x');
const outputIdx = parts[2]; const outputIdx = parts[2];

View File

@@ -21,7 +21,7 @@ export namespace ILightningApi {
export interface Channel { export interface Channel {
channel_id: string; channel_id: string;
chan_point: string; chan_point: string;
last_update: number | null; last_update: number;
node1_pub: string; node1_pub: string;
node2_pub: string; node2_pub: string;
capacity: string; capacity: string;
@@ -36,11 +36,11 @@ export namespace ILightningApi {
fee_rate_milli_msat: string; fee_rate_milli_msat: string;
disabled: boolean; disabled: boolean;
max_htlc_msat: string; max_htlc_msat: string;
last_update: number | null; last_update: number;
} }
export interface Node { export interface Node {
last_update: number | null; last_update: number;
pub_key: string; pub_key: string;
alias: string; alias: string;
addresses: { addresses: {

View File

@@ -33,7 +33,7 @@ class MempoolBlocks {
return this.mempoolBlockDeltas; return this.mempoolBlockDeltas;
} }
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] { public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
const latestMempool = memPool; const latestMempool = memPool;
const memPoolArray: TransactionExtended[] = []; const memPoolArray: TransactionExtended[] = [];
for (const i in latestMempool) { for (const i in latestMempool) {
@@ -75,14 +75,10 @@ class MempoolBlocks {
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds'); logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks); const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
if (saveResults) { this.mempoolBlocks = blocks;
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks); this.mempoolBlockDeltas = deltas;
this.mempoolBlocks = blocks;
this.mempoolBlockDeltas = deltas;
}
return blocks;
} }
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] { private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
@@ -97,14 +93,14 @@ class MempoolBlocks {
blockSize += tx.size; blockSize += tx.size;
transactions.push(tx); transactions.push(tx);
} else { } else {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
blockWeight = tx.weight; blockWeight = tx.weight;
blockSize = tx.size; blockSize = tx.size;
transactions = [tx]; transactions = [tx];
} }
}); });
if (transactions.length) { if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, mempoolBlocks.length)); mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
} }
return mempoolBlocks; return mempoolBlocks;
@@ -147,7 +143,7 @@ class MempoolBlocks {
return mempoolBlockDeltas; return mempoolBlockDeltas;
} }
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }): Promise<void> {
// 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: { [txid: string]: ThreadTransaction } = {}; const strippedMempool: { [txid: string]: ThreadTransaction } = {};
@@ -188,21 +184,19 @@ class MempoolBlocks {
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool }); this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
const { blocks, clusters } = await workerResultPromise; const { blocks, clusters } = await workerResultPromise;
this.processBlockTemplates(newMempool, blocks, clusters);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} 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;
} }
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> { public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[]): Promise<void> {
if (!this.txSelectionWorker) { if (!this.txSelectionWorker) {
// need to reset the worker // need to reset the worker
this.makeBlockTemplates(newMempool, saveResults); return this.makeBlockTemplates(newMempool);
return;
} }
// 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
@@ -230,16 +224,16 @@ class MempoolBlocks {
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed }); this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
const { blocks, clusters } = await workerResultPromise; const { blocks, clusters } = await workerResultPromise;
this.processBlockTemplates(newMempool, blocks, clusters);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
} 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, clusters, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool, blocks, clusters): void {
// update this thread's mempool with the results // update this thread's mempool with the results
blocks.forEach(block => { blocks.forEach(block => {
block.forEach(tx => { block.forEach(tx => {
@@ -255,7 +249,7 @@ class MempoolBlocks {
cluster.forEach(txid => { cluster.forEach(txid => {
if (txid === tx.txid) { if (txid === tx.txid) {
matched = true; matched = true;
} else { } else if (mempool[txid]) {
const relative = { const relative = {
txid: txid, txid: txid,
fee: mempool[txid].fee, fee: mempool[txid].fee,
@@ -281,29 +275,27 @@ class MempoolBlocks {
const mempoolBlocks = blocks.map((transactions, blockIndex) => { const mempoolBlocks = blocks.map((transactions, blockIndex) => {
return this.dataToMempoolBlocks(transactions.map(tx => { return this.dataToMempoolBlocks(transactions.map(tx => {
return mempool[tx.txid] || null; return mempool[tx.txid] || null;
}).filter(tx => !!tx), blockIndex); }).filter(tx => !!tx), undefined, undefined, blockIndex);
}); });
if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
}
return mempoolBlocks; this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
} }
private dataToMempoolBlocks(transactions: TransactionExtended[], blocksIndex: number): MempoolBlockWithTransactions { private dataToMempoolBlocks(transactions: TransactionExtended[],
let totalSize = 0; blockSize: number | undefined, blockWeight: number | undefined, blocksIndex: number): MempoolBlockWithTransactions {
let totalWeight = 0; let totalSize = blockSize || 0;
const fitTransactions: TransactionExtended[] = []; let totalWeight = blockWeight || 0;
transactions.forEach(tx => { if (blockSize === undefined && blockWeight === undefined) {
totalSize += tx.size; totalSize = 0;
totalWeight += tx.weight; totalWeight = 0;
if ((totalWeight + tx.weight) <= config.MEMPOOL.BLOCK_WEIGHT_UNITS * 1.2) { transactions.forEach(tx => {
fitTransactions.push(tx); totalSize += tx.size;
} totalWeight += tx.weight;
}); });
}
let rangeLength = 4; let rangeLength = 4;
if (blocksIndex === 0) { if (blocksIndex === 0) {
rangeLength = 8; rangeLength = 8;
@@ -321,7 +313,7 @@ class MempoolBlocks {
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: Common.getFeesInRange(transactions, rangeLength), feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid), transactionIds: transactions.map((tx) => tx.txid),
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)), transactions: transactions.map((tx) => Common.stripTransaction(tx)),
}; };
} }
} }

View File

@@ -10,33 +10,32 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
class Mempool { export class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000; protected static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30; protected static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false; protected inSync: boolean = false;
private mempoolCacheDelta: number = -1; protected mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {}; protected mempoolCache: { [txId: string]: TransactionExtended } = {};
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, protected mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], protected mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined; deletedTransactions: TransactionExtended[]) => void) | undefined;
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[], protected asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined; deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
private txPerSecondArray: number[] = []; protected txPerSecondArray: number[] = [];
private txPerSecond: number = 0; protected txPerSecond: number = 0;
private vBytesPerSecondArray: VbytesPerSecond[] = []; protected vBytesPerSecondArray: VbytesPerSecond[] = [];
private vBytesPerSecond: number = 0; protected vBytesPerSecond: number = 0;
private mempoolProtection = 0; protected mempoolProtection = 0;
private latestTransactions: any[] = []; protected latestTransactions: any[] = [];
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100;
private SAMPLE_TIME = 10000; // In ms
private timer = new Date().getTime();
private missingTxCount = 0;
constructor() { constructor() {
this.init();
}
protected init(): void {
setInterval(this.updateTxPerSecond.bind(this), 1000); setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000); setInterval(this.deleteExpiredTransactions.bind(this), 20000);
} }
@@ -133,16 +132,6 @@ class Mempool {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100); loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
} }
// https://github.com/mempool/mempool/issues/3283
const logEsplora404 = (missingTxCount, threshold, time) => {
const log = `In the past ${time / 1000} seconds, esplora tx API replied ${missingTxCount} times with a 404 error code while updating nodejs backend mempool`;
if (missingTxCount >= threshold) {
logger.warn(log);
} else if (missingTxCount > 0) {
logger.debug(log);
}
};
for (const txid of transactions) { for (const txid of transactions) {
if (!this.mempoolCache[txid]) { if (!this.mempoolCache[txid]) {
try { try {
@@ -157,10 +146,7 @@ class Mempool {
} }
hasChange = true; hasChange = true;
newTransactions.push(transaction); newTransactions.push(transaction);
} catch (e: any) { } catch (e) {
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
this.missingTxCount++;
}
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
} }
} }
@@ -170,14 +156,6 @@ class Mempool {
} }
} }
// Reset esplora 404 counter and log a warning if needed
const elapsedTime = new Date().getTime() - this.timer;
if (elapsedTime > this.SAMPLE_TIME) {
logEsplora404(this.missingTxCount, this.ESPLORA_MISSING_TX_WARNING_THRESHOLD, elapsedTime);
this.timer = new Date().getTime();
this.missingTxCount = 0;
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion // Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0 if (this.mempoolProtection === 0
&& currentMempoolSize > 20000 && currentMempoolSize > 20000
@@ -243,7 +221,7 @@ class Mempool {
} }
} }
private updateTxPerSecond() { protected updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD); const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan); this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0; this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;
@@ -256,7 +234,7 @@ class Mempool {
} }
} }
private deleteExpiredTransactions() { protected deleteExpiredTransactions() {
const now = new Date().getTime(); const now = new Date().getTime();
for (const tx in this.mempoolCache) { for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter; const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
@@ -267,7 +245,7 @@ class Mempool {
} }
} }
private $getMempoolInfo() { protected $getMempoolInfo() {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) { if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
return Promise.all([ return Promise.all([
bitcoinClient.getMempoolInfo(), bitcoinClient.getMempoolInfo(),

View File

@@ -1,13 +1,13 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from "../../config"; import config from "../../config";
import logger from '../../logger'; import logger from '../../logger';
import audits from '../audit';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import BlocksRepository from '../../repositories/BlocksRepository'; import BlocksRepository from '../../repositories/BlocksRepository';
import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository'; import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjustmentsRepository';
import HashratesRepository from '../../repositories/HashratesRepository'; import HashratesRepository from '../../repositories/HashratesRepository';
import bitcoinClient from '../bitcoin/bitcoin-client'; import bitcoinClient from '../bitcoin/bitcoin-client';
import mining from "./mining"; import mining from "./mining";
import PricesRepository from '../../repositories/PricesRepository';
class MiningRoutes { class MiningRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
@@ -32,27 +32,9 @@ class MiningRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/score/:hash', this.$getBlockAuditScore)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/timestamp/:timestamp', this.$getHeightFromTimestamp)
.get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice)
; ;
} }
private async $getHistoricalPrice(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
if (req.query.timestamp) {
res.status(200).send(await PricesRepository.$getNearestHistoricalPrice(
parseInt(<string>req.query.timestamp ?? 0, 10)
));
} else {
res.status(200).send(await PricesRepository.$getHistoricalPrices());
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getPool(req: Request, res: Response): Promise<void> { private async $getPool(req: Request, res: Response): Promise<void> {
try { try {
const stats = await mining.$getPoolStat(req.params.slug); const stats = await mining.$getPoolStat(req.params.slug);
@@ -263,7 +245,7 @@ class MiningRoutes {
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
if (!audit) { if (!audit) {
res.status(204).send(`This block has not been audited.`); res.status(404).send(`This block has not been audited.`);
return; return;
} }

View File

@@ -11,14 +11,12 @@ import DifficultyAdjustmentsRepository from '../../repositories/DifficultyAdjust
import config from '../../config'; import config from '../../config';
import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
import PricesRepository from '../../repositories/PricesRepository'; import PricesRepository from '../../repositories/PricesRepository';
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database';
class Mining { class Mining {
private blocksPriceIndexingRunning = false; blocksPriceIndexingRunning = false;
public lastHashrateIndexingDate: number | null = null;
public lastWeeklyHashrateIndexingDate: number | null = null; constructor() {
}
/** /**
* Get historical block predictions match rate * Get historical block predictions match rate
@@ -102,7 +100,6 @@ class Mining {
rank: rank++, rank: rank++,
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0, emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
slug: poolInfo.slug, slug: poolInfo.slug,
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
}; };
poolsStats.push(poolStat); poolsStats.push(poolStat);
}); });
@@ -118,7 +115,7 @@ class Mining {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
} }
return poolsStatistics; return poolsStatistics;
@@ -142,14 +139,11 @@ class Mining {
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w'); const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id);
const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id);
let currentEstimatedHashrate = 0; let currentEstimatedHashrate = 0;
try { try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h); currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
} }
return { return {
@@ -166,8 +160,6 @@ class Mining {
}, },
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h), estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null, reportedHashrate: null,
avgBlockHealth: avgHealth,
totalReward: totalReward,
}; };
} }
@@ -179,26 +171,25 @@ class Mining {
} }
/** /**
* Generate weekly mining pool hashrate history * [INDEXING] Generate weekly mining pool hashrate history
*/ */
public async $generatePoolHashrateHistory(): Promise<void> { public async $generatePoolHashrateHistory(): Promise<void> {
const now = new Date(); const now = new Date();
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
// Run only if: // Run only if:
// * this.lastWeeklyHashrateIndexingDate is set to null (node backend restart, reorg) // * lastestRunDate is set to 0 (node backend restart, reorg)
// * we started a new week (around Monday midnight) // * we started a new week (around Monday midnight)
const runIndexing = this.lastWeeklyHashrateIndexingDate === null || const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
now.getUTCDay() === 1 && this.lastWeeklyHashrateIndexingDate !== now.getUTCDate();
if (!runIndexing) { if (!runIndexing) {
logger.debug(`Pool hashrate history indexing is up to date, nothing to do`, logger.tags.mining);
return; return;
} }
try { try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp; const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000; const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps(); const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
const hashrates: any[] = []; const hashrates: any[] = [];
@@ -214,7 +205,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`, logger.tags.mining); logger.debug(`Indexing weekly mining pool hashrate`);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0); loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -251,7 +242,7 @@ class Mining {
}); });
} }
newlyIndexed += hashrates.length / Math.max(1, pools.length); newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0; hashrates.length = 0;
} }
@@ -262,7 +253,7 @@ class Mining {
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false); loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -272,36 +263,36 @@ class Mining {
++indexedThisRun; ++indexedThisRun;
++totalIndexed; ++totalIndexed;
} }
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate(); await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.info(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining); logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
} else { } else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining); logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
} }
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e; throw e;
} }
} }
/** /**
* Generate daily hashrate data * [INDEXING] Generate daily hashrate data
*/ */
public async $generateNetworkHashrateHistory(): Promise<void> { public async $generateNetworkHashrateHistory(): Promise<void> {
// We only run this once a day around midnight // We only run this once a day around midnight
const today = new Date().getUTCDate(); const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
if (today === this.lastHashrateIndexingDate) { const now = new Date().getUTCDate();
logger.debug(`Network hashrate history indexing is up to date, nothing to do`, logger.tags.mining); if (now === latestRunDate) {
return; return;
} }
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp; const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try { try {
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.timestamp * 1000; const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date()); const lastMidnight = this.getDateMidnight(new Date());
let toTimestamp = Math.round(lastMidnight.getTime()); let toTimestamp = Math.round(lastMidnight.getTime());
@@ -314,7 +305,7 @@ class Mining {
const startedAt = new Date().getTime() / 1000; const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`, logger.tags.mining); logger.debug(`Indexing daily network hashrate`);
loadingIndicators.setProgress('daily-hashrate-indexing', 0); loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
@@ -352,7 +343,7 @@ class Mining {
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining); logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress); loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -377,16 +368,16 @@ class Mining {
newlyIndexed += hashrates.length; newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates); await HashratesRepository.$saveHashrates(hashrates);
this.lastHashrateIndexingDate = new Date().getUTCDate(); await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.info(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} else { } else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining); logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
} }
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining); logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e; throw e;
} }
} }
@@ -402,13 +393,13 @@ class Mining {
} }
const blocks: any = await BlocksRepository.$getBlocksDifficulty(); const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(await bitcoinClient.getBlockHash(0)); const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty; let currentDifficulty = genesisBlock.difficulty;
let totalIndexed = 0; let totalIndexed = 0;
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) { if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
await DifficultyAdjustmentsRepository.$saveAdjustments({ await DifficultyAdjustmentsRepository.$saveAdjustments({
time: genesisBlock.timestamp, time: genesisBlock.time,
height: 0, height: 0,
difficulty: currentDifficulty, difficulty: currentDifficulty,
adjustment: 0.0, adjustment: 0.0,
@@ -452,13 +443,13 @@ 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.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining); logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
} }
} }
if (totalIndexed > 0) { if (totalIndexed > 0) {
logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} else { } else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining); logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
} }
@@ -467,7 +458,7 @@ class Mining {
/** /**
* Create a link between blocks and the latest price at when they were mined * Create a link between blocks and the latest price at when they were mined
*/ */
public async $indexBlockPrices(): Promise<void> { public async $indexBlockPrices() {
if (this.blocksPriceIndexingRunning === true) { if (this.blocksPriceIndexingRunning === true) {
return; return;
} }
@@ -505,7 +496,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr, logger.tags.mining); logger.debug(logStr);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
blocksPrices.length = 0; blocksPrices.length = 0;
} }
@@ -517,7 +508,7 @@ class Mining {
if (blocksWithoutPrices.length > 200000) { if (blocksWithoutPrices.length > 200000) {
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`; logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
} }
logger.debug(logStr, logger.tags.mining); logger.debug(logStr);
await BlocksRepository.$saveBlockPrices(blocksPrices); await BlocksRepository.$saveBlockPrices(blocksPrices);
} }
} catch (e) { } catch (e) {
@@ -528,41 +519,6 @@ class Mining {
this.blocksPriceIndexingRunning = false; this.blocksPriceIndexingRunning = false;
} }
/**
* Index core coinstatsindex
*/
public async $indexCoinStatsIndex(): Promise<void> {
let timer = new Date().getTime() / 1000;
let totalIndexed = 0;
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks;
while (currentBlockHeight > 0) {
const indexedBlocks = await BlocksRepository.$getBlocksMissingCoinStatsIndex(
currentBlockHeight, currentBlockHeight - 10000);
for (const block of indexedBlocks) {
const txoutset = await bitcoinClient.getTxoutSetinfo('none', block.height);
await BlocksRepository.$updateCoinStatsIndexData(block.hash, txoutset.txouts,
Math.round(txoutset.block_info.prevout_spent * 100000000));
++totalIndexed;
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
if (elapsedSeconds > 5) {
logger.info(`Indexing coinstatsindex data for block #${block.height}. Indexed ${totalIndexed} blocks.`, logger.tags.mining);
timer = new Date().getTime() / 1000;
}
}
currentBlockHeight -= 10000;
}
if (totalIndexed) {
logger.info(`Indexing missing coinstatsindex data completed`, logger.tags.mining);
}
}
private getDateMidnight(date: Date): Date { private getDateMidnight(date: Date): Date {
date.setUTCHours(0); date.setUTCHours(0);
date.setUTCMinutes(0); date.setUTCMinutes(0);
@@ -574,7 +530,6 @@ class Mining {
private getTimeRange(interval: string | null, scale = 1): number { private getTimeRange(interval: string | null, scale = 1): number {
switch (interval) { switch (interval) {
case '4y': return 43200 * scale; // 12h
case '3y': return 43200 * scale; // 12h case '3y': return 43200 * scale; // 12h
case '2y': return 28800 * scale; // 8h case '2y': return 28800 * scale; // 8h
case '1y': return 28800 * scale; // 8h case '1y': return 28800 * scale; // 8h

View File

@@ -1,161 +1,289 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import config from '../config'; import config from '../config';
import PoolsRepository from '../repositories/PoolsRepository'; import BlocksRepository from '../repositories/BlocksRepository';
import { PoolTag } from '../mempool.interfaces';
import diskCache from './disk-cache'; interface Pool {
name: string;
link: string;
regexes: string[];
addresses: string[];
slug: string;
}
class PoolsParser { class PoolsParser {
miningPools: any[] = []; miningPools: any[] = [];
unknownPool: any = { unknownPool: any = {
'id': 0,
'name': 'Unknown', 'name': 'Unknown',
'link': 'https://learnmeabitcoin.com/technical/coinbase-transaction', 'link': 'https://learnmeabitcoin.com/technical/coinbase-transaction',
'regexes': '[]', 'regexes': '[]',
'addresses': '[]', 'addresses': '[]',
'slug': 'unknown' 'slug': 'unknown'
}; };
private uniqueLogs: string[] = []; slugWarnFlag = false;
private uniqueLog(loggerFunction: any, msg: string): void {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
public setMiningPools(pools): void {
for (const pool of pools) {
pool.regexes = pool.tags;
pool.slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
delete(pool.tags);
}
this.miningPools = pools;
}
/** /**
* Populate our db with updated mining pool definition * Parse the pools.json file, consolidate the data and dump it into the database
* @param pools
*/ */
public async migratePoolsJson(): Promise<void> { public async migratePoolsJson(poolsJson: object): Promise<void> {
// We also need to wipe the backend cache to make sure we don't serve blocks with if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
// the wrong mining pool (usually happen with unknown blocks) return;
diskCache.wipeCache(); }
await this.$insertUnknownPool(); // First we save every entries without paying attention to pool duplication
const poolsDuplicated: Pool[] = [];
for (const pool of this.miningPools) { const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
if (!pool.id) { for (let i = 0; i < coinbaseTags.length; ++i) {
logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`); poolsDuplicated.push({
continue; 'name': (<Pool>coinbaseTags[i][1]).name,
'link': (<Pool>coinbaseTags[i][1]).link,
'regexes': [coinbaseTags[i][0]],
'addresses': [],
'slug': ''
});
}
const addressesTags = Object.entries(poolsJson['payout_addresses']);
for (let i = 0; i < addressesTags.length; ++i) {
poolsDuplicated.push({
'name': (<Pool>addressesTags[i][1]).name,
'link': (<Pool>addressesTags[i][1]).link,
'regexes': [],
'addresses': [addressesTags[i][0]],
'slug': ''
});
}
// Then, we find unique mining pool names
const poolNames: string[] = [];
for (let i = 0; i < poolsDuplicated.length; ++i) {
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
poolNames.push(poolsDuplicated[i].name);
}
}
logger.debug(`Found ${poolNames.length} unique mining pools`, logger.tags.mining);
// Get existing pools from the db
let existingPools;
try {
if (config.DATABASE.ENABLED === true) {
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
} else {
existingPools = [];
}
} catch (e) {
logger.err('Cannot get existing pools from the database, skipping pools.json import', logger.tags.mining);
return;
}
this.miningPools = [];
// Finally, we generate the final consolidated pools data
const finalPoolDataAdd: Pool[] = [];
const finalPoolDataUpdate: Pool[] = [];
const finalPoolDataRename: Pool[] = [];
for (let i = 0; i < poolNames.length; ++i) {
let allAddresses: string[] = [];
let allRegexes: string[] = [];
const match = poolsDuplicated.filter((pool: Pool) => pool.name === poolNames[i]);
for (let y = 0; y < match.length; ++y) {
allAddresses = allAddresses.concat(match[y].addresses);
allRegexes = allRegexes.concat(match[y].regexes);
} }
const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false); const finalPoolName = poolNames[i].replace(`'`, `''`); // To support single quote in names when doing db queries
if (!poolDB) {
// New mining pool let slug: string | undefined;
const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); try {
logger.debug(`Inserting new mining pool ${pool.name}`); slug = poolsJson['slugs'][poolNames[i]];
await PoolsRepository.$insertNewMiningPool(pool, slug); } catch (e) {
await this.$deleteUnknownBlocks(); if (this.slugWarnFlag === false) {
logger.warn(`pools.json does not seem to contain the 'slugs' object`, logger.tags.mining);
this.slugWarnFlag = true;
}
}
if (slug === undefined) {
// Only keep alphanumerical
slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase();
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`, logger.tags.mining);
}
const poolObj = {
'name': finalPoolName,
'link': match[0].link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
};
const existingPool = existingPools.find((pool) => pool.name === poolNames[i]);
if (existingPool !== undefined) {
// Check if any data was actually updated
const equals = (a, b) =>
a.length === b.length &&
a.every((v, i) => v === b[i]);
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
finalPoolDataUpdate.push(poolObj);
}
} else { } else {
if (poolDB.name !== pool.name) { // Double check that if we're not just renaming a pool (same address same regex)
// Pool has been renamed const [poolToRename]: any[] = await DB.query(`
const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); SELECT * FROM pools
logger.warn(`Renaming ${poolDB.name} mining pool to ${pool.name}. Slug has been updated. Maybe you want to make a redirection from 'https://mempool.space/mining/pool/${poolDB.slug}' to 'https://mempool.space/mining/pool/${newSlug}`); WHERE addresses = ? OR regexes = ?`,
await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name); [JSON.stringify(poolObj.addresses), JSON.stringify(poolObj.regexes)]
);
if (poolToRename && poolToRename.length > 0) {
// We're actually renaming an existing pool
finalPoolDataRename.push({
'name': poolObj.name,
'link': poolObj.link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
});
logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`, logger.tags.mining);
} else {
logger.debug(`Add '${finalPoolName}' mining pool`, logger.tags.mining);
finalPoolDataAdd.push(poolObj);
} }
if (poolDB.link !== pool.link) { }
// Pool link has changed
logger.debug(`Updating link for ${pool.name} mining pool`); this.miningPools.push({
await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link); 'name': finalPoolName,
'link': match[0].link,
'regexes': JSON.stringify(allRegexes),
'addresses': JSON.stringify(allAddresses),
'slug': slug
});
}
if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info('Mining pools.json import completed (no database)', logger.tags.mining);
return;
}
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 ||
finalPoolDataRename.length > 0
) {
logger.debug(`Update pools table now`, logger.tags.mining);
// Add new mining pools into the database
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
}
queryAdd = queryAdd.slice(0, -1) + ';';
// Updated existing mining pools in the database
const updateQueries: string[] = [];
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
updateQueries.push(`
UPDATE pools
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
slug='${finalPoolDataUpdate[i].slug}'
WHERE name='${finalPoolDataUpdate[i].name}'
;`);
}
// Rename mining pools
const renameQueries: string[] = [];
for (let i = 0; i < finalPoolDataRename.length; ++i) {
renameQueries.push(`
UPDATE pools
SET name='${finalPoolDataRename[i].name}', link='${finalPoolDataRename[i].link}',
slug='${finalPoolDataRename[i].slug}'
WHERE regexes='${JSON.stringify(finalPoolDataRename[i].regexes)}'
AND addresses='${JSON.stringify(finalPoolDataRename[i].addresses)}'
;`);
}
try {
if (finalPoolDataAdd.length > 0 || updateQueries.length > 0) {
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
} }
if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
JSON.stringify(pool.regexes) !== poolDB.regexes) { if (finalPoolDataAdd.length > 0) {
// Pool addresses changed or coinbase tags changed await DB.query({ sql: queryAdd, timeout: 120000 });
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool. If 'AUTOMATIC_BLOCK_REINDEXING' is enabled, we will re-index its blocks and 'unknown' blocks`);
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
await this.$deleteBlocksForPool(poolDB);
} }
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
for (const query of renameQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool();
logger.info('Mining pools.json import completed', logger.tags.mining);
} catch (e) {
logger.err(`Cannot import pools in the database`, logger.tags.mining);
throw e;
} }
} }
logger.info('Mining pools-v2.json import completed'); try {
await this.insertUnknownPool();
} catch (e) {
logger.err(`Cannot insert unknown pool in the database`, logger.tags.mining);
throw e;
}
} }
/** /**
* Manually add the 'unknown pool' * Manually add the 'unknown pool'
*/ */
public async $insertUnknownPool(): Promise<void> { private async insertUnknownPool() {
if (!config.DATABASE.ENABLED) {
return;
}
try { try {
const [rows]: any[] = await DB.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 }); const [rows]: any[] = await DB.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 });
if (rows.length === 0) { if (rows.length === 0) {
await DB.query({ await DB.query({
sql: `INSERT INTO pools(name, link, regexes, addresses, slug, unique_id) sql: `INSERT INTO pools(name, link, regexes, addresses, slug)
VALUES("${this.unknownPool.name}", "${this.unknownPool.link}", "[]", "[]", "${this.unknownPool.slug}", 0); VALUES("Unknown", "https://learnmeabitcoin.com/technical/coinbase-transaction", "[]", "[]", "unknown");
`}); `});
} else { } else {
await DB.query(`UPDATE pools await DB.query(`UPDATE pools
SET name='${this.unknownPool.name}', link='${this.unknownPool.link}', SET name='Unknown', link='https://learnmeabitcoin.com/technical/coinbase-transaction',
regexes='[]', addresses='[]', regexes='[]', addresses='[]',
slug='${this.unknownPool.slug}', slug='unknown'
unique_id=0 WHERE name='Unknown'
WHERE slug='${this.unknownPool.slug}'
`); `);
} }
} catch (e) { } catch (e) {
logger.err(`Unable to insert or update "Unknown" mining pool. Reason: ${e instanceof Error ? e.message : e}`); logger.err('Unable to insert "Unknown" mining pool', logger.tags.mining);
} }
} }
/** /**
* Delete indexed blocks for an updated mining pool * Delete blocks which needs to be reindexed
*
* @param pool
*/ */
private async $deleteBlocksForPool(pool: PoolTag): Promise<void> { private async $deleteBlocskToReindex(finalPoolDataUpdate: any[]) {
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) { if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
return; return;
} }
// Get oldest blocks mined by the pool and assume pools-v2.json updates only concern most recent years const blockCount = await BlocksRepository.$blockCount(null, null);
// Ignore early days of Bitcoin as there were no mining pool yet if (blockCount === 0) {
const [oldestPoolBlock]: any[] = await DB.query(` return;
SELECT height }
FROM blocks
WHERE pool_id = ?
ORDER BY height
LIMIT 1`,
[pool.id]
);
const oldestBlockHeight = oldestPoolBlock.length ?? 0 > 0 ? oldestPoolBlock[0].height : 130635;
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height ${oldestBlockHeight} for re-indexing`);
await DB.query(`
DELETE FROM blocks
WHERE pool_id = ? AND height >= ${oldestBlockHeight}`,
[unknownPool[0].id]
);
logger.notice(`Deleting blocks from ${pool.name} mining pool for re-indexing`);
await DB.query(`
DELETE FROM blocks
WHERE pool_id = ?`,
[pool.id]
);
}
private async $deleteUnknownBlocks(): Promise<void> { for (const updatedPool of finalPoolDataUpdate) {
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
if (pool.length > 0) {
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`, logger.tags.mining);
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
}
}
// Ignore early days of Bitcoin as there were not mining pool yet
logger.notice(`Deleting blocks with unknown mining pool from height 130635 for future re-indexing`, logger.tags.mining);
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`); const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
this.uniqueLog(logger.notice, `Deleting blocks with unknown mining pool from height 130635 for re-indexing`); await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
await DB.query(`
DELETE FROM blocks logger.notice(`Truncating hashrates for future re-indexing`, logger.tags.mining);
WHERE pool_id = ? AND height >= 130635`, await DB.query(`DELETE FROM hashrates`);
[unknownPool[0].id]
);
} }
} }

View File

@@ -375,17 +375,6 @@ class StatisticsApi {
} }
} }
public async $list4Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(43200, '4 YEAR'); // 12h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
} catch (e) {
logger.err('$list4Y() error' + (e instanceof Error ? e.message : e));
return [];
}
}
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] { private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => { return statistic.map((s) => {
return { return {

View File

@@ -14,11 +14,10 @@ class StatisticsRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
; ;
} }
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) { private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) {
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@@ -55,9 +54,6 @@ class StatisticsRoutes {
case '3y': case '3y':
result = await statisticsApi.$list3Y(); result = await statisticsApi.$list3Y();
break; break;
case '4y':
result = await statisticsApi.$list4Y();
break;
default: default:
result = await statisticsApi.$list2H(); result = await statisticsApi.$list2H();
} }

View File

@@ -14,7 +14,6 @@ class TransactionUtils {
vout: tx.vout vout: tx.vout
.map((vout) => ({ .map((vout) => ({
scriptpubkey_address: vout.scriptpubkey_address, scriptpubkey_address: vout.scriptpubkey_address,
scriptpubkey_asm: vout.scriptpubkey_asm,
value: vout.value value: vout.value
})) }))
.filter((vout) => vout.value) .filter((vout) => vout.value)

View File

@@ -1,13 +1,14 @@
import logger from '../logger'; import logger from '../logger';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { import {
BlockExtended, TransactionExtended, WebsocketResponse, BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators OptimizedStatistic, ILoadingIndicators, IConversionRates
} from '../mempool.interfaces'; } from '../mempool.interfaces';
import blocks from './blocks'; import blocks from './blocks';
import memPool from './mempool'; import memPool from './mempool';
import backendInfo from './backend-info'; import backendInfo from './backend-info';
import mempoolBlocks from './mempool-blocks'; import mempoolBlocks from './mempool-blocks';
import fiatConversion from './fiat-conversion';
import { Common } from './common'; import { Common } from './common';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import config from '../config'; import config from '../config';
@@ -18,9 +19,6 @@ import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import Audit from './audit'; import Audit from './audit';
import { deepClone } from '../utils/clone';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -194,7 +192,7 @@ class WebsocketHandler {
}); });
} }
handleNewConversionRates(conversionRates: ApiPrice) { handleNewConversionRates(conversionRates: IConversionRates) {
if (!this.wss) { if (!this.wss) {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
@@ -215,7 +213,7 @@ class WebsocketHandler {
'mempoolInfo': memPool.getMempoolInfo(), 'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(), 'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks, 'blocks': _blocks,
'conversions': priceUpdater.getLatestPrices(), 'conversions': fiatConversion.getConversionRates(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(), 'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(), 'backendInfo': backendInfo.getBackendInfo(),
@@ -253,9 +251,9 @@ class WebsocketHandler {
} }
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true); await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid));
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool);
} }
const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlocks = mempoolBlocks.getMempoolBlocks();
@@ -420,57 +418,47 @@ class WebsocketHandler {
const _memPool = memPool.getMempool(); const _memPool = memPool.getMempool();
if (config.MEMPOOL.AUDIT) { if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
let projectedBlocks; await mempoolBlocks.makeBlockTemplates(_memPool);
// template calculation functions have mempool side effects, so calculate audits using } else {
// a cloned copy of the mempool if we're running a different algorithm for mempool updates mempoolBlocks.updateMempoolBlocks(_memPool);
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool); }
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
} else {
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
}
if (Common.indexingEnabled() && memPool.isInSync()) { if (Common.indexingEnabled() && memPool.isInSync()) {
const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
const matchRate = Math.round(score * 100 * 100) / 100;
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => { const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
return { const matchRate = Math.round(score * 100 * 100) / 100;
txid: tx.txid,
vsize: tx.vsize,
fee: tx.fee ? Math.round(tx.fee) : 0,
value: tx.value,
};
}) : [];
BlocksSummariesRepository.$saveTemplate({ const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
height: block.height, return {
template: { txid: tx.txid,
id: block.id, vsize: tx.vsize,
transactions: stripped fee: tx.fee ? Math.round(tx.fee) : 0,
} value: tx.value,
}); };
}) : [];
BlocksAuditsRepository.$saveAudit({ BlocksSummariesRepository.$saveTemplate({
time: block.timestamp, height: block.height,
height: block.height, template: {
hash: block.id, id: block.id,
addedTxs: added, transactions: stripped
missingTxs: censored,
freshTxs: fresh,
matchRate: matchRate,
});
if (block.extras) {
block.extras.matchRate = matchRate;
block.extras.similarity = similarity;
} }
} });
} else if (block.extras) {
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); BlocksAuditsRepository.$saveAudit({
if (mBlocks?.length && mBlocks[0].transactions) { time: block.timestamp,
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions); height: block.height,
hash: block.id,
addedTxs: added,
missingTxs: censored,
freshTxs: fresh,
matchRate: matchRate,
});
if (block.extras) {
block.extras.matchRate = matchRate;
} }
} }
@@ -483,9 +471,9 @@ class WebsocketHandler {
} }
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true); await mempoolBlocks.updateBlockTemplates(_memPool, [], removed);
} else { } else {
mempoolBlocks.updateMempoolBlocks(_memPool, true); mempoolBlocks.updateMempoolBlocks(_memPool);
} }
const mBlocks = mempoolBlocks.getMempoolBlocks(); const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();

View File

@@ -19,6 +19,7 @@ interface IConfig {
MEMPOOL_BLOCKS_AMOUNT: number; MEMPOOL_BLOCKS_AMOUNT: number;
INDEXING_BLOCKS_AMOUNT: number; INDEXING_BLOCKS_AMOUNT: number;
BLOCKS_SUMMARIES_INDEXING: boolean; BLOCKS_SUMMARIES_INDEXING: boolean;
PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean; USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[]; EXTERNAL_ASSETS: string[];
EXTERNAL_MAX_RETRY: number; EXTERNAL_MAX_RETRY: number;
@@ -28,11 +29,10 @@ interface IConfig {
AUTOMATIC_BLOCK_REINDEXING: boolean; AUTOMATIC_BLOCK_REINDEXING: boolean;
POOLS_JSON_URL: string, POOLS_JSON_URL: string,
POOLS_JSON_TREE_URL: string, POOLS_JSON_TREE_URL: string,
AUDIT: boolean;
ADVANCED_GBT_AUDIT: boolean; ADVANCED_GBT_AUDIT: boolean;
ADVANCED_GBT_MEMPOOL: boolean; ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean; CPFP_INDEXING: boolean;
MAX_BLOCKS_BULK_QUERY: number; RBF_DUAL_NODE: boolean;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@@ -141,6 +141,7 @@ const defaults: IConfig = {
'MEMPOOL_BLOCKS_AMOUNT': 8, 'MEMPOOL_BLOCKS_AMOUNT': 8,
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks 'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
'BLOCKS_SUMMARIES_INDEXING': false, 'BLOCKS_SUMMARIES_INDEXING': false,
'PRICE_FEED_UPDATE_INTERVAL': 600,
'USE_SECOND_NODE_FOR_MINFEE': false, 'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [], 'EXTERNAL_ASSETS': [],
'EXTERNAL_MAX_RETRY': 1, 'EXTERNAL_MAX_RETRY': 1,
@@ -148,13 +149,12 @@ const defaults: IConfig = {
'USER_AGENT': 'mempool', 'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug', 'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false, 'AUTOMATIC_BLOCK_REINDEXING': false,
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
'AUDIT': false,
'ADVANCED_GBT_AUDIT': false, 'ADVANCED_GBT_AUDIT': false,
'ADVANCED_GBT_MEMPOOL': false, 'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false, 'CPFP_INDEXING': false,
'MAX_BLOCKS_BULK_QUERY': 0, 'RBF_DUAL_NODE': false,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',

View File

@@ -24,8 +24,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
private checkDBFlag() { private checkDBFlag() {
if (config.DATABASE.ENABLED === false) { if (config.DATABASE.ENABLED === false) {
const stack = new Error().stack; logger.err('Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue');
logger.err(`Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue.\nStack trace: ${stack}}`);
} }
} }

View File

@@ -10,12 +10,14 @@ import memPool from './api/mempool';
import diskCache from './api/disk-cache'; import diskCache from './api/disk-cache';
import statistics from './api/statistics/statistics'; import statistics from './api/statistics/statistics';
import websocketHandler from './api/websocket-handler'; import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq/bisq'; import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets'; import bisqMarkets from './api/bisq/markets';
import logger from './logger'; import logger from './logger';
import backendInfo from './api/backend-info'; import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators'; import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool'; import mempool from './api/mempool';
import altMempool from './api/alt-mempool';
import elementsParser from './api/liquid/elements-parser'; import elementsParser from './api/liquid/elements-parser';
import databaseMigration from './api/database-migration'; import databaseMigration from './api/database-migration';
import syncAssets from './sync-assets'; import syncAssets from './sync-assets';
@@ -35,11 +37,6 @@ import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher'; import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service'; import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips';
import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -47,11 +44,6 @@ class Server {
private app: Application; private app: Application;
private currentBackendRetryInterval = 5; private currentBackendRetryInterval = 5;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
constructor() { constructor() {
this.app = express(); this.app = express();
@@ -87,18 +79,6 @@ class Server {
async startServer(worker = false): Promise<void> { async startServer(worker = false): Promise<void> {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
if (config.DATABASE.ENABLED) {
await DB.checkDbConnection();
try {
if (process.env.npm_config_reindex_blocks === 'true') { // Re-index requests
await databaseMigration.$blocksReindexingTruncate();
}
await databaseMigration.$initializeOrMigrateDatabase();
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
}
}
this.app this.app
.use((req: Request, res: Response, next: NextFunction) => { .use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
@@ -108,21 +88,34 @@ class Server {
.use(express.text({ type: ['text/plain', 'application/base64'] })) .use(express.text({ type: ['text/plain', 'application/base64'] }))
; ;
if (config.DATABASE.ENABLED) {
await priceUpdater.$initializeLatestPriceWithDb();
}
this.server = http.createServer(this.app); this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server }); this.wss = new WebSocket.Server({ server: this.server });
this.setUpWebsocketHandling(); this.setUpWebsocketHandling();
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) {
diskCache.loadMempoolCache(); diskCache.loadMempoolCache();
} }
if (config.DATABASE.ENABLED) {
await DB.checkDbConnection();
try {
if (process.env.npm_config_reindex !== undefined) { // Re-index requests
const tables = process.env.npm_config_reindex.split(',');
logger.warn(`Indexed data for "${process.env.npm_config_reindex}" tables will be erased in 5 seconds (using '--reindex')`);
await Common.sleep$(5000);
await databaseMigration.$truncateIndexedData(tables);
}
await databaseMigration.$initializeOrMigrateDatabase();
if (Common.indexingEnabled()) {
await indexer.$resetHashratesIndexingState();
}
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
}
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
statistics.startStatistics(); statistics.startStatistics();
} }
@@ -135,8 +128,7 @@ class Server {
} }
} }
priceUpdater.$run(); fiatConversion.startService();
await chainTips.updateOrphanedBlocks();
this.setUpHttpApiRoutes(); this.setUpHttpApiRoutes();
@@ -144,8 +136,6 @@ class Server {
this.runMainUpdateLoop(); this.runMainUpdateLoop();
} }
setInterval(() => { this.healthCheck(); }, 2500);
if (config.BISQ.ENABLED) { if (config.BISQ.ENABLED) {
bisq.startBisqService(); bisq.startBisqService();
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
@@ -178,30 +168,25 @@ class Server {
logger.debug(msg); logger.debug(msg);
} }
} }
await poolsUpdater.updatePoolsJson();
await blocks.$updateBlocks(); await blocks.$updateBlocks();
await memPool.$updateMempool(); await memPool.$updateMempool();
if (config.MEMPOOL.RBF_DUAL_NODE) {
await altMempool.$updateMempool();
}
indexer.$run(); indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.currentBackendRetryInterval = 5; this.currentBackendRetryInterval = 5;
} catch (e: any) { } catch (e) {
let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`; const loggerMsg = `runMainLoop error: ${(e instanceof Error ? e.message : e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
if (e?.stack) {
loggerMsg += ` Stack trace: ${e.stack}`;
}
// 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
// Maximum retry delay is 60 seconds
if (this.currentBackendRetryInterval > 5) { if (this.currentBackendRetryInterval > 5) {
logger.warn(loggerMsg); logger.warn(loggerMsg);
mempool.setOutOfSync(); mempool.setOutOfSync();
} else { } else {
logger.debug(loggerMsg); logger.debug(loggerMsg);
} }
if (e instanceof AxiosError) { logger.debug(JSON.stringify(e));
logger.debug(`AxiosError: ${e?.message}`);
}
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval); setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
this.currentBackendRetryInterval *= 2; this.currentBackendRetryInterval *= 2;
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60); this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
@@ -212,8 +197,8 @@ class Server {
try { try {
await fundingTxFetcher.$init(); await fundingTxFetcher.$init();
await networkSyncService.$startService(); await networkSyncService.$startService();
await lightningStatsUpdater.$startService();
await forensicsService.$startService(); await forensicsService.$startService();
await lightningStatsUpdater.$startService();
} catch(e) { } catch(e) {
logger.err(`Nodejs lightning backend crashed. 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);
@@ -240,7 +225,7 @@ class Server {
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)); fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
} }
@@ -264,26 +249,6 @@ class Server {
channelsRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app);
} }
} }
healthCheck(): void {
const now = Date.now();
const stats = v8.getHeapStatistics();
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
const warnThreshold = 0.8 * stats.heap_size_limit;
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true;
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)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
this.warnedHeapCritical = false;
this.maxHeapSize = 0;
this.lastHeapLogTime = now;
}
}
} }
((): Server => new Server())(); ((): Server => new Server())();

View File

@@ -3,71 +3,23 @@ import blocks from './api/blocks';
import mempool from './api/mempool'; import mempool from './api/mempool';
import mining from './api/mining/mining'; import mining from './api/mining/mining';
import logger from './logger'; import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository';
import bitcoinClient from './api/bitcoin/bitcoin-client'; import bitcoinClient from './api/bitcoin/bitcoin-client';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import PricesRepository from './repositories/PricesRepository'; import PricesRepository from './repositories/PricesRepository';
export interface CoreIndex {
name: string;
synced: boolean;
best_block_height: number;
}
class Indexer { class Indexer {
runIndexer = true; runIndexer = true;
indexerRunning = false; indexerRunning = false;
tasksRunning: string[] = []; tasksRunning: string[] = [];
coreIndexes: CoreIndex[] = [];
/** public reindex() {
* Check which core index is available for indexing
*/
public async checkAvailableCoreIndexes(): Promise<void> {
const updatedCoreIndexes: CoreIndex[] = [];
const indexes: any = await bitcoinClient.getIndexInfo();
for (const indexName in indexes) {
const newState = {
name: indexName,
synced: indexes[indexName].synced,
best_block_height: indexes[indexName].best_block_height,
};
logger.info(`Core index '${indexName}' is ${indexes[indexName].synced ? 'synced' : 'not synced'}. Best block height is ${indexes[indexName].best_block_height}`);
updatedCoreIndexes.push(newState);
if (indexName === 'coinstatsindex' && newState.synced === true) {
const previousState = this.isCoreIndexReady('coinstatsindex');
// if (!previousState || previousState.synced === false) {
this.runSingleTask('coinStatsIndex');
// }
}
}
this.coreIndexes = updatedCoreIndexes;
}
/**
* Return the best block height if a core index is available, or 0 if not
*
* @param name
* @returns
*/
public isCoreIndexReady(name: string): CoreIndex | null {
for (const index of this.coreIndexes) {
if (index.name === name && index.synced === true) {
return index;
}
}
return null;
}
public reindex(): void {
if (Common.indexingEnabled()) { if (Common.indexingEnabled()) {
this.runIndexer = true; this.runIndexer = true;
} }
} }
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> { public async runSingleTask(task: 'blocksPrices') {
if (!Common.indexingEnabled()) { if (!Common.indexingEnabled()) {
return; return;
} }
@@ -76,27 +28,20 @@ class Indexer {
this.tasksRunning.push(task); this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId(); const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) { if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining); logger.debug(`Blocks prices indexer is waiting for the price updater to complete`)
setTimeout(() => { setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
}, 10000); }, 10000);
} else { } else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining); logger.debug(`Blocks prices indexer will run now`)
await mining.$indexBlockPrices(); await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
} }
} }
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
}
} }
public async $run(): Promise<void> { public async $run() {
if (!Common.indexingEnabled() || this.runIndexer === false || if (!Common.indexingEnabled() || this.runIndexer === false ||
this.indexerRunning === true || mempool.hasPriority() this.indexerRunning === true || mempool.hasPriority()
) { ) {
@@ -114,15 +59,13 @@ class Indexer {
logger.debug(`Running mining indexer`); logger.debug(`Running mining indexer`);
await this.checkAvailableCoreIndexes();
try { try {
await priceUpdater.$run(); await priceUpdater.$run();
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining); logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`);
setTimeout(() => this.reindex(), 10000); setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false; this.indexerRunning = false;
return; return;
@@ -130,6 +73,7 @@ class Indexer {
this.runSingleTask('blocksPrices'); this.runSingleTask('blocksPrices');
await mining.$indexDifficultyAdjustments(); await mining.$indexDifficultyAdjustments();
await this.$resetHashratesIndexingState(); // TODO - Remove this as it's not efficient
await mining.$generateNetworkHashrateHistory(); await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory(); await mining.$generatePoolHashrateHistory();
await blocks.$generateBlocksSummariesDatabase(); await blocks.$generateBlocksSummariesDatabase();
@@ -148,6 +92,16 @@ class Indexer {
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`); logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
setTimeout(() => this.reindex(), runEvery); setTimeout(() => this.reindex(), runEvery);
} }
async $resetHashratesIndexingState() {
try {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', 0);
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
} }
export default new Indexer(); export default new Indexer();

View File

@@ -1,10 +1,8 @@
import { IEsploraApi } from './api/bitcoin/esplora-api.interface'; import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
import { OrphanedBlock } from './api/chain-tips'; import { HeapNode } from "./utils/pairing-heap";
import { HeapNode } from './utils/pairing-heap';
export interface PoolTag { export interface PoolTag {
id: number; id: number; // mysql row id
uniqueId: number;
name: string; name: string;
link: string; link: string;
regexes: string; // JSON array regexes: string; // JSON array
@@ -18,7 +16,6 @@ export interface PoolInfo {
link: string; link: string;
blockCount: number; blockCount: number;
slug: string; slug: string;
avgMatchRate: number | null;
} }
export interface PoolStats extends PoolInfo { export interface PoolStats extends PoolInfo {
@@ -66,7 +63,6 @@ interface VinStrippedToScriptsig {
interface VoutStrippedToScriptPubkey { interface VoutStrippedToScriptPubkey {
scriptpubkey_address: string | undefined; scriptpubkey_address: string | undefined;
scriptpubkey_asm: string | undefined;
value: number; value: number;
} }
@@ -148,45 +144,23 @@ export interface TransactionStripped {
} }
export interface BlockExtension { export interface BlockExtension {
totalFees: number; totalFees?: number;
medianFee: number; // median fee rate medianFee?: number;
feeRange: number[]; // fee rate percentiles feeRange?: number[];
reward: number; reward?: number;
matchRate: number | null; coinbaseTx?: TransactionMinerInfo;
similarity?: number; matchRate?: number;
pool: { pool?: {
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` id: number;
name: string; name: string;
slug: string; slug: string;
}; };
avgFee: number; avgFee?: number;
avgFeeRate: number; avgFeeRate?: number;
coinbaseRaw: string; coinbaseRaw?: string;
orphans: OrphanedBlock[] | null; usd?: number | null;
coinbaseAddress: string | null;
coinbaseSignature: string | null;
coinbaseSignatureAscii: string | null;
virtualSize: number;
avgTxSize: number;
totalInputs: number;
totalOutputs: number;
totalOutputAmt: number;
medianFeeAmt: number | null; // median fee in sats
feePercentiles: number[] | null, // fee percentiles in sats
segwitTotalTxs: number;
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;
totalInputAmt: number | null;
} }
/**
* Note: Everything that is added in here will be automatically returned through
* /api/v1/block and /api/v1/blocks APIs
*/
export interface BlockExtended extends IEsploraApi.Block { export interface BlockExtended extends IEsploraApi.Block {
extras: BlockExtension; extras: BlockExtension;
} }
@@ -294,6 +268,7 @@ interface RequiredParams {
} }
export interface ILoadingIndicators { [name: string]: number; } export interface ILoadingIndicators { [name: string]: number; }
export interface IConversionRates { [currency: string]: number; }
export interface IBackendInfo { export interface IBackendInfo {
hostname: string; hostname: string;

View File

@@ -1,7 +1,8 @@
import { BlockExtended, BlockExtension, BlockPrice } from '../mempool.interfaces'; import { BlockExtended, 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';
import { prepareBlock } from '../utils/blocks-utils';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository'; import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2'; import { escape } from 'mysql2';
@@ -9,90 +10,27 @@ import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client'; import bitcoinClient from '../api/bitcoin/bitcoin-client';
import config from '../config'; import config from '../config';
import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
const BLOCK_DB_FIELDS = `
blocks.hash AS id,
blocks.height,
blocks.version,
UNIX_TIMESTAMP(blocks.blockTimestamp) AS timestamp,
blocks.bits,
blocks.nonce,
blocks.difficulty,
blocks.merkle_root,
blocks.tx_count,
blocks.size,
blocks.weight,
blocks.previous_block_hash AS previousblockhash,
UNIX_TIMESTAMP(blocks.median_timestamp) AS mediantime,
blocks.fees AS totalFees,
blocks.median_fee AS medianFee,
blocks.fee_span AS feeRange,
blocks.reward,
pools.unique_id AS poolId,
pools.name AS poolName,
pools.slug AS poolSlug,
blocks.avg_fee AS avgFee,
blocks.avg_fee_rate AS avgFeeRate,
blocks.coinbase_raw AS coinbaseRaw,
blocks.coinbase_address AS coinbaseAddress,
blocks.coinbase_signature AS coinbaseSignature,
blocks.coinbase_signature_ascii AS coinbaseSignatureAscii,
blocks.avg_tx_size AS avgTxSize,
blocks.total_inputs AS totalInputs,
blocks.total_outputs AS totalOutputs,
blocks.total_output_amt AS totalOutputAmt,
blocks.median_fee_amt AS medianFeeAmt,
blocks.fee_percentiles AS feePercentiles,
blocks.segwit_total_txs AS segwitTotalTxs,
blocks.segwit_total_size AS segwitTotalSize,
blocks.segwit_total_weight AS segwitTotalWeight,
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmts
`;
class BlocksRepository { class BlocksRepository {
/** /**
* Save indexed block data in the database * Save indexed block data in the database
*/ */
public async $saveBlockInDatabase(block: BlockExtended) { public async $saveBlockInDatabase(block: BlockExtended) {
const truncatedCoinbaseSignature = block?.extras?.coinbaseSignature?.substring(0, 500);
const truncatedCoinbaseSignatureAscii = block?.extras?.coinbaseSignatureAscii?.substring(0, 500);
try { try {
const query = `INSERT INTO blocks( const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size, height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty, weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee, pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce, reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate, merkle_root, previous_block_hash, avg_fee, avg_fee_rate
median_timestamp, header, coinbase_address,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii
) VALUE ( ) VALUE (
?, ?, FROM_UNIXTIME(?), ?, ?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?
FROM_UNIXTIME(?), ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?
)`; )`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
if (!poolDbId) {
throw Error(`Could not find a mining pool with the unique_id = ${block.extras.pool.id}. This error should never be printed.`);
}
const params: any[] = [ const params: any[] = [
block.height, block.height,
block.id, block.id,
@@ -102,7 +40,7 @@ class BlocksRepository {
block.tx_count, block.tx_count,
block.extras.coinbaseRaw, block.extras.coinbaseRaw,
block.difficulty, block.difficulty,
poolDbId.id, block.extras.pool?.id, // Should always be set to something
block.extras.totalFees, block.extras.totalFees,
JSON.stringify(block.extras.feeRange), JSON.stringify(block.extras.feeRange),
block.extras.medianFee, block.extras.medianFee,
@@ -114,63 +52,19 @@ class BlocksRepository {
block.previousblockhash, block.previousblockhash,
block.extras.avgFee, block.extras.avgFee,
block.extras.avgFeeRate, block.extras.avgFeeRate,
block.mediantime,
block.extras.header,
block.extras.coinbaseAddress,
truncatedCoinbaseSignature,
block.extras.utxoSetSize,
block.extras.utxoSetChange,
block.extras.avgTxSize,
block.extras.totalInputs,
block.extras.totalOutputs,
block.extras.totalInputAmt,
block.extras.totalOutputAmt,
block.extras.feePercentiles ? JSON.stringify(block.extras.feePercentiles) : null,
block.extras.segwitTotalTxs,
block.extras.segwitTotalSize,
block.extras.segwitTotalWeight,
block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii,
]; ];
await DB.query(query, params); await DB.query(query, params);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`, logger.tags.mining); logger.debug(`$saveBlockInDatabase() - Block ${block.height} has already been indexed, ignoring`);
} else { } else {
logger.err('Cannot save indexed block into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot save indexed block into db. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
} }
/**
* Save newly indexed data from core coinstatsindex
*
* @param utxoSetSize
* @param totalInputAmt
*/
public async $updateCoinStatsIndexData(blockHash: string, utxoSetSize: number,
totalInputAmt: number
) : Promise<void> {
try {
const query = `
UPDATE blocks
SET utxoset_size = ?, total_input_amt = ?
WHERE hash = ?
`;
const params: any[] = [
utxoSetSize,
totalInputAmt,
blockHash
];
await DB.query(query, params);
} catch (e: any) {
logger.err('Cannot update indexed block coinstatsindex. 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]
*/ */
@@ -330,55 +224,6 @@ class BlocksRepository {
} }
} }
/**
* Get average block health for all blocks for a single pool
*/
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
FROM blocks
JOIN blocks_audits ON blocks.height = blocks_audits.height
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].avg_match_rate) {
return 0;
}
return Math.round(rows[0].avg_match_rate * 100) / 100;
} catch (e) {
logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get average block health for all blocks for a single pool
*/
public async $getTotalRewardForPoolId(poolId: number): Promise<number> {
const params: any[] = [];
const query = `
SELECT sum(reward) as total_reward
FROM blocks
WHERE blocks.pool_id = ?
`;
params.push(poolId);
try {
const [rows] = await DB.query(query, params);
if (!rows[0] || !rows[0].total_reward) {
return 0;
}
return rows[0].total_reward;
} catch (e) {
logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Get the oldest indexed block * Get the oldest indexed block
*/ */
@@ -405,17 +250,34 @@ class BlocksRepository {
/** /**
* Get blocks mined by a specific mining pool * Get blocks mined by a specific mining pool
*/ */
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> { public async $getBlocksByPool(slug: string, startHeight?: number): Promise<object[]> {
const pool = await PoolsRepository.$getPool(slug); const pool = await PoolsRepository.$getPool(slug);
if (!pool) { if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug)); throw new Error('This mining pool does not exist ' + escape(slug));
} }
const params: any[] = []; const params: any[] = [];
let query = ` let query = ` SELECT
SELECT ${BLOCK_DB_FIELDS} blocks.height,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
weight,
tx_count,
coinbase_raw,
difficulty,
fees,
fee_span,
median_fee,
reward,
version,
bits,
nonce,
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
FROM blocks FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE pool_id = ?`; WHERE pool_id = ?`;
params.push(pool.id); params.push(pool.id);
@@ -428,11 +290,11 @@ class BlocksRepository {
LIMIT 10`; LIMIT 10`;
try { try {
const [rows]: any[] = await DB.query(query, params); const [rows] = await DB.query(query, params);
const blocks: BlockExtended[] = []; const blocks: BlockExtended[] = [];
for (const block of rows) { for (const block of <object[]>rows) {
blocks.push(await this.formatDbBlockIntoExtendedBlock(block)); blocks.push(prepareBlock(block));
} }
return blocks; return blocks;
@@ -445,21 +307,46 @@ class BlocksRepository {
/** /**
* Get one block by height * Get one block by height
*/ */
public async $getBlockByHeight(height: number): Promise<BlockExtended | null> { public async $getBlockByHeight(height: number): Promise<object | null> {
try { try {
const [rows]: any[] = await DB.query(` const [rows]: any[] = await DB.query(`SELECT
SELECT ${BLOCK_DB_FIELDS} blocks.height,
hash,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,
weight,
tx_count,
coinbase_raw,
difficulty,
pools.id as pool_id,
pools.name as pool_name,
pools.link as pool_link,
pools.slug as pool_slug,
pools.addresses as pool_addresses,
pools.regexes as pool_regexes,
fees,
fee_span,
median_fee,
reward,
version,
bits,
nonce,
merkle_root,
previous_block_hash as previousblockhash,
avg_fee,
avg_fee_rate
FROM blocks FROM blocks
JOIN pools ON blocks.pool_id = pools.id JOIN pools ON blocks.pool_id = pools.id
WHERE blocks.height = ?`, WHERE blocks.height = ${height}
[height] `);
);
if (rows.length <= 0) { if (rows.length <= 0) {
return null; return null;
} }
return await this.formatDbBlockIntoExtendedBlock(rows[0]); rows[0].fee_span = JSON.parse(rows[0].fee_span);
return 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;
@@ -472,7 +359,10 @@ class BlocksRepository {
public async $getBlockByHash(hash: string): Promise<object | null> { public async $getBlockByHash(hash: string): Promise<object | null> {
try { try {
const query = ` const query = `
SELECT ${BLOCK_DB_FIELDS} SELECT *, blocks.height, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
FROM blocks FROM blocks
JOIN pools ON blocks.pool_id = pools.id JOIN pools ON blocks.pool_id = pools.id
WHERE hash = ?; WHERE hash = ?;
@@ -482,8 +372,9 @@ class BlocksRepository {
if (rows.length <= 0) { if (rows.length <= 0) {
return null; return null;
} }
return await this.formatDbBlockIntoExtendedBlock(rows[0]); rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) { } catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
@@ -574,15 +465,8 @@ class BlocksRepository {
public async $validateChain(): Promise<boolean> { public async $validateChain(): Promise<boolean> {
try { try {
const start = new Date().getTime(); const start = new Date().getTime();
const [blocks]: any[] = await DB.query(` const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
SELECT UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
height,
hash,
previous_block_hash,
UNIX_TIMESTAMP(blockTimestamp) AS timestamp
FROM blocks
ORDER BY height
`);
let partialMsg = false; let partialMsg = false;
let idx = 1; let idx = 1;
@@ -797,7 +681,6 @@ class BlocksRepository {
SELECT height SELECT height
FROM compact_cpfp_clusters FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ? WHERE height <= ? AND height >= ?
GROUP BY height
ORDER BY height DESC; ORDER BY height DESC;
`, [currentBlockHeight, minHeight]); `, [currentBlockHeight, minHeight]);
@@ -811,6 +694,7 @@ class BlocksRepository {
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
return [];
} }
/** /**
@@ -857,7 +741,7 @@ class BlocksRepository {
try { try {
let query = `INSERT INTO blocks_prices(height, price_id) VALUES`; let query = `INSERT INTO blocks_prices(height, price_id) VALUES`;
for (const price of blockPrices) { for (const price of blockPrices) {
query += ` (${price.height}, ${price.priceId}),`; query += ` (${price.height}, ${price.priceId}),`
} }
query = query.slice(0, -1); query = query.slice(0, -1);
await DB.query(query); await DB.query(query);
@@ -870,132 +754,6 @@ class BlocksRepository {
} }
} }
} }
/**
* Get all indexed blocsk with missing coinstatsindex data
*/
public async $getBlocksMissingCoinStatsIndex(maxHeight: number, minHeight: number): Promise<any> {
try {
const [blocks] = await DB.query(`
SELECT height, hash
FROM blocks
WHERE height >= ${minHeight} AND height <= ${maxHeight} AND
(utxoset_size IS NULL OR total_input_amt IS NULL)
`);
return blocks;
} catch (e) {
logger.err(`Cannot get blocks with missing coinstatsindex. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Save indexed median fee to avoid recomputing it later
*
* @param id
* @param feePercentiles
*/
public async $saveFeePercentilesForBlockId(id: string, feePercentiles: number[]): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET fee_percentiles = ?, median_fee_amt = ?
WHERE hash = ?`,
[JSON.stringify(feePercentiles), feePercentiles[3], id]
);
} catch (e) {
logger.err(`Cannot update block fee_percentiles. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param
*
* @param dbBlk
*/
private async formatDbBlockIntoExtendedBlock(dbBlk: any): Promise<BlockExtended> {
const blk: Partial<BlockExtended> = {};
const extras: Partial<BlockExtension> = {};
// IEsploraApi.Block
blk.id = dbBlk.id;
blk.height = dbBlk.height;
blk.version = dbBlk.version;
blk.timestamp = dbBlk.timestamp;
blk.bits = dbBlk.bits;
blk.nonce = dbBlk.nonce;
blk.difficulty = dbBlk.difficulty;
blk.merkle_root = dbBlk.merkle_root;
blk.tx_count = dbBlk.tx_count;
blk.size = dbBlk.size;
blk.weight = dbBlk.weight;
blk.previousblockhash = dbBlk.previousblockhash;
blk.mediantime = dbBlk.mediantime;
// BlockExtension
extras.totalFees = dbBlk.totalFees;
extras.medianFee = dbBlk.medianFee;
extras.feeRange = JSON.parse(dbBlk.feeRange);
extras.reward = dbBlk.reward;
extras.pool = {
id: dbBlk.poolId,
name: dbBlk.poolName,
slug: dbBlk.poolSlug,
};
extras.avgFee = dbBlk.avgFee;
extras.avgFeeRate = dbBlk.avgFeeRate;
extras.coinbaseRaw = dbBlk.coinbaseRaw;
extras.coinbaseAddress = dbBlk.coinbaseAddress;
extras.coinbaseSignature = dbBlk.coinbaseSignature;
extras.coinbaseSignatureAscii = dbBlk.coinbaseSignatureAscii;
extras.avgTxSize = dbBlk.avgTxSize;
extras.totalInputs = dbBlk.totalInputs;
extras.totalOutputs = dbBlk.totalOutputs;
extras.totalOutputAmt = dbBlk.totalOutputAmt;
extras.medianFeeAmt = dbBlk.medianFeeAmt;
extras.feePercentiles = JSON.parse(dbBlk.feePercentiles);
extras.segwitTotalTxs = dbBlk.segwitTotalTxs;
extras.segwitTotalSize = dbBlk.segwitTotalSize;
extras.segwitTotalWeight = dbBlk.segwitTotalWeight;
extras.header = dbBlk.header,
extras.utxoSetChange = dbBlk.utxoSetChange;
extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0;
// Re-org can happen after indexing so we need to always get the
// latest state from core
extras.orphans = chainTips.getOrphanedBlocksAtHeight(dbBlk.height);
// Match rate is not part of the blocks table, but it is part of APIs so we must include it
extras.matchRate = null;
if (config.MEMPOOL.AUDIT) {
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(dbBlk.id);
if (auditScore != null) {
extras.matchRate = auditScore.matchRate;
}
}
// If we're missing block summary related field, check if we can populate them on the fly now
if (Common.blocksSummariesIndexingEnabled() &&
(extras.medianFeeAmt === null || extras.feePercentiles === null))
{
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveSummary({ height: block.height, mined: summary });
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
}
if (extras.feePercentiles !== null) {
extras.medianFeeAmt = extras.feePercentiles[3];
}
}
blk.extras = <BlockExtension>extras;
return <BlockExtended>blk;
}
} }
export default new BlocksRepository(); export default new BlocksRepository();

View File

@@ -80,48 +80,6 @@ class BlocksSummariesRepository {
logger.err('Cannot delete indexed blocks summaries. Reason: ' + (e instanceof Error ? e.message : 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
*
* @param id
*/
public async $getFeePercentilesByBlockId(id: string): Promise<number[] | null> {
try {
const [rows]: any[] = await DB.query(`
SELECT transactions
FROM blocks_summaries
WHERE id = ?`,
[id]
);
if (rows === null || rows.length === 0) {
return null;
}
const transactions = JSON.parse(rows[0].transactions);
if (transactions === null) {
return null;
}
transactions.shift(); // Ignore coinbase
transactions.sort((a: any, b: any) => a.fee - b.fee);
const fees = transactions.map((t: any) => t.fee);
return [
fees[0] ?? 0, // min
fees[Math.max(0, Math.floor(fees.length * 0.1) - 1)] ?? 0, // 10th
fees[Math.max(0, Math.floor(fees.length * 0.25) - 1)] ?? 0, // 25th
fees[Math.max(0, Math.floor(fees.length * 0.5) - 1)] ?? 0, // median
fees[Math.max(0, Math.floor(fees.length * 0.75) - 1)] ?? 0, // 75th
fees[Math.max(0, Math.floor(fees.length * 0.9) - 1)] ?? 0, // 90th
fees[fees.length - 1] ?? 0, // max
];
} catch (e) {
logger.err(`Cannot get block summaries transactions. Reason: ` + (e instanceof Error ? e.message : e));
return null;
}
}
} }
export default new BlocksSummariesRepository(); export default new BlocksSummariesRepository();

View File

@@ -111,7 +111,7 @@ class CpfpRepository {
} }
} }
public async $getCluster(clusterRoot: string): Promise<Cluster | void> { public async $getCluster(clusterRoot: string): Promise<Cluster> {
const [clusterRows]: any = await DB.query( const [clusterRows]: any = await DB.query(
` `
SELECT * SELECT *
@@ -121,11 +121,8 @@ class CpfpRepository {
[clusterRoot] [clusterRoot]
); );
const cluster = clusterRows[0]; const cluster = clusterRows[0];
if (cluster?.txs) { cluster.txs = this.unpack(cluster.txs);
cluster.txs = this.unpack(cluster.txs); return cluster;
return cluster;
}
return;
} }
public async $deleteClustersFrom(height: number): Promise<void> { public async $deleteClustersFrom(height: number): Promise<void> {
@@ -139,9 +136,9 @@ class CpfpRepository {
[height] [height]
) as RowDataPacket[][]; ) as RowDataPacket[][];
if (rows?.length) { if (rows?.length) {
for (const clusterToDelete of rows) { for (let clusterToDelete of rows) {
const txs = this.unpack(clusterToDelete?.txs); const txs = this.unpack(clusterToDelete.txs);
for (const tx of txs) { for (let tx of txs) {
await transactionRepository.$removeTransaction(tx.txid); await transactionRepository.$removeTransaction(tx.txid);
} }
} }
@@ -207,25 +204,20 @@ class CpfpRepository {
return []; return [];
} }
try { const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); const txs: Ancestor[] = [];
const txs: Ancestor[] = []; const view = new DataView(arrayBuffer);
const view = new DataView(arrayBuffer); for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) {
for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) { const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join('');
const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join(''); const weight = view.getUint32(offset + 32);
const weight = view.getUint32(offset + 32); const fee = Number(view.getBigUint64(offset + 36));
const fee = Number(view.getBigUint64(offset + 36)); txs.push({
txs.push({ txid,
txid, weight,
weight, fee
fee });
});
}
return txs;
} catch (e) {
logger.warn(`Failed to unpack CPFP cluster. Reason: ` + (e instanceof Error ? e.message : e));
return [];
} }
return txs;
} }
} }

View File

@@ -1,4 +1,5 @@
import { Common } from '../api/common'; import { Common } from '../api/common';
import config from '../config';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { IndexedDifficultyAdjustment } from '../mempool.interfaces'; import { IndexedDifficultyAdjustment } from '../mempool.interfaces';
@@ -20,9 +21,9 @@ class DifficultyAdjustmentsRepository {
await DB.query(query, params); await DB.query(query, params);
} catch (e: any) { } catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`, logger.tags.mining); logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`);
} else { } else {
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`);
throw e; throw e;
} }
} }
@@ -54,7 +55,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -83,7 +84,7 @@ class DifficultyAdjustmentsRepository {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[]; return rows as IndexedDifficultyAdjustment[];
} catch (e) { } catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -93,27 +94,27 @@ class DifficultyAdjustmentsRepository {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`); const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height); return rows.map(block => block.height);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`);
throw e; throw e;
} }
} }
public async $deleteAdjustementsFromHeight(height: number): Promise<void> { public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
try { try {
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`, logger.tags.mining); logger.info(`Delete newer difficulty adjustments from height ${height} from the database`);
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]); await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e; throw e;
} }
} }
public async $deleteLastAdjustment(): Promise<void> { public async $deleteLastAdjustment(): Promise<void> {
try { try {
logger.info(`Delete last difficulty adjustment from the database`, logger.tags.mining); logger.info(`Delete last difficulty adjustment from the database`);
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`); await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e; throw e;
} }
} }

View File

@@ -1,6 +1,5 @@
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import { Common } from '../api/common'; import { Common } from '../api/common';
import mining from '../api/mining/mining';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
@@ -25,7 +24,7 @@ class HashratesRepository {
try { try {
await DB.query(query); await DB.query(query);
} catch (e: any) { } catch (e: any) {
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -51,7 +50,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -78,7 +77,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -93,7 +92,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp); return rows.map(row => row.timestamp);
} catch (e) { } catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -128,7 +127,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(query);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -158,7 +157,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [pool.id]); const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0]; boundaries = rows[0];
} catch (e) { } catch (e) {
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e));
} }
// Get hashrates entries between boundaries // Get hashrates entries between boundaries
@@ -173,7 +172,21 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]); const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Set latest run timestamp
*/
public async $setLatestRun(key: string, val: number) {
const query = `UPDATE state SET number = ? WHERE name = ?`;
try {
await DB.query(query, [val, key]);
} catch (e) {
logger.err(`Cannot set last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -192,7 +205,7 @@ class HashratesRepository {
} }
return rows[0]['number']; return rows[0]['number'];
} catch (e) { } catch (e) {
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
@@ -201,7 +214,7 @@ class HashratesRepository {
* Delete most recent data points for re-indexing * Delete most recent data points for re-indexing
*/ */
public async $deleteLastEntries() { public async $deleteLastEntries() {
logger.info(`Delete latest hashrates data points from the database`, logger.tags.mining); logger.info(`Delete latest hashrates data points from the database`);
try { try {
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`); const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
@@ -209,10 +222,10 @@ class HashratesRepository {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]); await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]);
} }
// Re-run the hashrate indexing to fill up missing data // Re-run the hashrate indexing to fill up missing data
mining.lastHashrateIndexingDate = null; await this.$setLatestRun('last_hashrates_indexing', 0);
mining.lastWeeklyHashrateIndexingDate = null; await this.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
} }
} }
@@ -225,10 +238,10 @@ class HashratesRepository {
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]);
// Re-run the hashrate indexing to fill up missing data // Re-run the hashrate indexing to fill up missing data
mining.lastHashrateIndexingDate = null; await this.$setLatestRun('last_hashrates_indexing', 0);
mining.lastWeeklyHashrateIndexingDate = null; await this.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) { } catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
} }
} }
} }

View File

@@ -1,5 +1,4 @@
import { Common } from '../api/common'; import { Common } from '../api/common';
import poolsParser from '../api/pools-parser';
import config from '../config'; import config from '../config';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
@@ -10,7 +9,7 @@ class PoolsRepository {
* Get all pools tagging info * Get all pools tagging info
*/ */
public async $getPools(): Promise<PoolTag[]> { public async $getPools(): Promise<PoolTag[]> {
const [rows] = await DB.query('SELECT id, unique_id as uniqueId, name, addresses, regexes, slug FROM pools'); const [rows] = await DB.query('SELECT id, name, addresses, regexes, slug FROM pools;');
return <PoolTag[]>rows; return <PoolTag[]>rows;
} }
@@ -18,11 +17,7 @@ class PoolsRepository {
* Get unknown pool tagging info * Get unknown pool tagging info
*/ */
public async $getUnknownPool(): Promise<PoolTag> { public async $getUnknownPool(): Promise<PoolTag> {
let [rows]: any[] = await DB.query('SELECT id, unique_id as uniqueId, name, slug FROM pools where name = "Unknown"'); const [rows] = await DB.query('SELECT id, name, slug FROM pools where name = "Unknown"');
if (rows && rows.length === 0 && config.DATABASE.ENABLED) {
await poolsParser.$insertUnknownPool();
[rows] = await DB.query('SELECT id, unique_id as uniqueId, name, slug FROM pools where name = "Unknown"');
}
return <PoolTag>rows[0]; return <PoolTag>rows[0];
} }
@@ -32,25 +27,16 @@ class PoolsRepository {
public async $getPoolsInfo(interval: string | null = null): Promise<PoolInfo[]> { public async $getPoolsInfo(interval: string | null = null): Promise<PoolInfo[]> {
interval = Common.getSqlInterval(interval); interval = Common.getSqlInterval(interval);
let query = ` let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link, slug
SELECT
COUNT(blocks.height) As blockCount,
pool_id AS poolId,
pools.name AS name,
pools.link AS link,
slug,
AVG(blocks_audits.match_rate) AS avgMatchRate
FROM blocks FROM blocks
JOIN pools on pools.id = pool_id JOIN pools on pools.id = pool_id`;
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
`;
if (interval) { if (interval) {
query += ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; query += ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
} }
query += ` GROUP BY pool_id query += ` GROUP BY pool_id
ORDER BY COUNT(blocks.height) DESC`; ORDER BY COUNT(height) DESC`;
try { try {
const [rows] = await DB.query(query); const [rows] = await DB.query(query);
@@ -64,7 +50,7 @@ class PoolsRepository {
/** /**
* Get basic pool info and block count between two timestamp * Get basic pool info and block count between two timestamp
*/ */
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> { public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
FROM pools FROM pools
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?) LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
@@ -80,9 +66,9 @@ class PoolsRepository {
} }
/** /**
* Get a mining pool info * Get mining pool statistics for one pool
*/ */
public async $getPool(slug: string, parse: boolean = true): Promise<PoolTag | null> { public async $getPool(slug: string): Promise<PoolTag | null> {
const query = ` const query = `
SELECT * SELECT *
FROM pools FROM pools
@@ -95,44 +81,10 @@ class PoolsRepository {
return null; return null;
} }
if (parse) { rows[0].regexes = JSON.parse(rows[0].regexes);
rows[0].regexes = JSON.parse(rows[0].regexes);
}
if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
rows[0].addresses = []; // pools-v2.json only contains mainnet addresses
} else if (parse) {
rows[0].addresses = JSON.parse(rows[0].addresses);
}
return rows[0];
} catch (e) {
logger.err('Cannot get pool from db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get a mining pool info by its unique id
*/
public async $getPoolByUniqueId(id: number, parse: boolean = true): Promise<PoolTag | null> {
const query = `
SELECT *
FROM pools
WHERE pools.unique_id = ?`;
try {
const [rows]: any[] = await DB.query(query, [id]);
if (rows.length < 1) {
return null;
}
if (parse) {
rows[0].regexes = JSON.parse(rows[0].regexes);
}
if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
rows[0].addresses = []; // pools.json only contains mainnet addresses rows[0].addresses = []; // pools.json only contains mainnet addresses
} else if (parse) { } else {
rows[0].addresses = JSON.parse(rows[0].addresses); rows[0].addresses = JSON.parse(rows[0].addresses);
} }
@@ -142,84 +94,6 @@ class PoolsRepository {
throw e; throw e;
} }
} }
/**
* Insert a new mining pool in the database
*
* @param pool
*/
public async $insertNewMiningPool(pool: any, slug: string): Promise<void> {
try {
await DB.query(`
INSERT INTO pools
SET name = ?, link = ?, addresses = ?, regexes = ?, slug = ?, unique_id = ?`,
[pool.name, pool.link, JSON.stringify(pool.addresses), JSON.stringify(pool.regexes), slug, pool.id]
);
} catch (e: any) {
logger.err(`Cannot insert new mining pool into db. Reason: ` + (e instanceof Error ? e.message : e));
}
}
/**
* Rename an existing mining pool
*
* @param dbId
* @param newSlug
* @param newName
*/
public async $renameMiningPool(dbId: number, newSlug: string, newName: string): Promise<void> {
try {
await DB.query(`
UPDATE pools
SET slug = ?, name = ?
WHERE id = ?`,
[newSlug, newName, dbId]
);
} catch (e: any) {
logger.err(`Cannot rename mining pool id ${dbId}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
/**
* Update an exisiting mining pool link
*
* @param dbId
* @param newLink
*/
public async $updateMiningPoolLink(dbId: number, newLink: string): Promise<void> {
try {
await DB.query(`
UPDATE pools
SET link = ?
WHERE id = ?`,
[newLink, dbId]
);
} catch (e: any) {
logger.err(`Cannot update link for mining pool id ${dbId}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
/**
* Update an existing mining pool addresses or coinbase tags
*
* @param dbId
* @param addresses
* @param regexes
*/
public async $updateMiningPoolTags(dbId: number, addresses: string, regexes: string): Promise<void> {
try {
await DB.query(`
UPDATE pools
SET addresses = ?, regexes = ?
WHERE id = ?`,
[JSON.stringify(addresses), JSON.stringify(regexes), dbId]
);
} catch (e: any) {
logger.err(`Cannot update mining pool id ${dbId}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
} }
export default new PoolsRepository(); export default new PoolsRepository();

View File

@@ -1,228 +1,50 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import priceUpdater from '../tasks/price-updater'; import { Prices } from '../tasks/price-updater';
export interface ApiPrice {
time?: number,
USD: number,
EUR: number,
GBP: number,
CAD: number,
CHF: number,
AUD: number,
JPY: number,
}
const ApiPriceFields = `
UNIX_TIMESTAMP(time) as time,
USD,
EUR,
GBP,
CAD,
CHF,
AUD,
JPY
`;
export interface ExchangeRates {
USDEUR: number,
USDGBP: number,
USDCAD: number,
USDCHF: number,
USDAUD: number,
USDJPY: number,
}
export interface Conversion {
prices: ApiPrice[],
exchangeRates: ExchangeRates;
}
export const MAX_PRICES = {
USD: 100000000,
EUR: 100000000,
GBP: 100000000,
CAD: 100000000,
CHF: 100000000,
AUD: 100000000,
JPY: 10000000000,
};
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: ApiPrice): Promise<void> { public async $savePrices(time: number, prices: Prices): Promise<void> {
if (prices.USD === -1) { if (prices.USD === -1) {
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues // Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine // As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
return; return;
} }
// Sanity check
for (const currency of Object.keys(prices)) {
if (prices[currency] < -1 || prices[currency] > MAX_PRICES[currency]) { // We use -1 to mark a "missing data, so it's a valid entry"
logger.info(`Ignore BTC${currency} price of ${prices[currency]}`);
prices[currency] = 0;
}
}
try { try {
await DB.query(` await DB.query(`
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY) INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`, VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY] [time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
); );
} catch (e) { } catch (e: any) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;
} }
} }
public async $getOldestPriceTime(): Promise<number> { public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(` const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time
LIMIT 1
`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getLatestPriceId(): Promise<number | null> { public async $getLatestPriceId(): Promise<number | null> {
const [oldestRow] = await DB.query(` const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
SELECT id
FROM prices
ORDER BY time DESC
LIMIT 1`
);
return oldestRow[0] ? oldestRow[0].id : null; return oldestRow[0] ? oldestRow[0].id : null;
} }
public async $getLatestPriceTime(): Promise<number> { public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(` const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
ORDER BY time DESC
LIMIT 1`
);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getPricesTimes(): Promise<number[]> { public async $getPricesTimes(): Promise<number[]> {
const [times] = await DB.query(` const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time`);
SELECT UNIX_TIMESTAMP(time) AS time
FROM prices
WHERE USD != -1
ORDER BY time
`);
if (!Array.isArray(times)) {
return [];
}
return times.map(time => time.time); return times.map(time => time.time);
} }
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> { public async $getPricesTimesAndId(): Promise<number[]> {
const [times] = await DB.query(` const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
SELECT return times;
UNIX_TIMESTAMP(time) AS time,
id,
USD
FROM prices
ORDER BY time
`);
return times as {time: number, id: number, USD: number}[];
}
public async $getLatestConversionRates(): Promise<ApiPrice> {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
LIMIT 1`
);
if (!Array.isArray(rates) || rates.length === 0) {
return priceUpdater.getEmptyPricesObj();
}
return rates[0] as ApiPrice;
}
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
try {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
WHERE UNIX_TIMESTAMP(time) < ?
ORDER BY time DESC
LIMIT 1`,
[timestamp]
);
if (!Array.isArray(rates)) {
throw Error(`Cannot get single historical price from the database`);
}
// Compute fiat exchange rates
let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = {
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
};
return {
prices: rates as ApiPrice[],
exchangeRates: exchangeRates
};
} catch (e) {
logger.err(`Cannot fetch single historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
return null;
}
}
public async $getHistoricalPrices(): Promise<Conversion | null> {
try {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
ORDER BY time DESC
`);
if (!Array.isArray(rates)) {
throw Error(`Cannot get average historical price from the database`);
}
// Compute fiat exchange rates
let latestPrice = rates[0] as ApiPrice;
if (latestPrice.USD === -1) {
latestPrice = priceUpdater.getEmptyPricesObj();
}
const computeFx = (usd: number, other: number): number =>
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
const exchangeRates: ExchangeRates = {
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
};
return {
prices: rates as ApiPrice[],
exchangeRates: exchangeRates
};
} catch (e) {
logger.err(`Cannot fetch historical prices from the db. Reason ${e instanceof Error ? e.message : e}`);
return null;
}
} }
} }

View File

@@ -3,6 +3,15 @@ import logger from '../logger';
import { Ancestor, CpfpInfo } from '../mempool.interfaces'; import { Ancestor, CpfpInfo } from '../mempool.interfaces';
import cpfpRepository from './CpfpRepository'; import cpfpRepository from './CpfpRepository';
interface CpfpSummary {
txid: string;
cluster: string;
root: string;
txs: Ancestor[];
height: number;
fee_rate: number;
}
class TransactionRepository { class TransactionRepository {
public async $setCluster(txid: string, clusterRoot: string): Promise<void> { public async $setCluster(txid: string, clusterRoot: string): Promise<void> {
try { try {
@@ -63,9 +72,7 @@ class TransactionRepository {
const txid = txRows[0].id.toLowerCase(); const txid = txRows[0].id.toLowerCase();
const clusterId = txRows[0].root.toLowerCase(); const clusterId = txRows[0].root.toLowerCase();
const cluster = await cpfpRepository.$getCluster(clusterId); const cluster = await cpfpRepository.$getCluster(clusterId);
if (cluster) { return this.convertCpfp(txid, cluster);
return this.convertCpfp(txid, cluster);
}
} }
} catch (e) { } catch (e) {
logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot get transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
@@ -74,18 +81,13 @@ class TransactionRepository {
} }
public async $removeTransaction(txid: string): Promise<void> { public async $removeTransaction(txid: string): Promise<void> {
try { await DB.query(
await DB.query( `
` DELETE FROM compact_transactions
DELETE FROM compact_transactions WHERE txid = UNHEX(?)
WHERE txid = UNHEX(?) `,
`, [txid]
[txid] );
);
} catch (e) {
logger.warn('Cannot delete transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
} }
private convertCpfp(txid, cluster): CpfpInfo { private convertCpfp(txid, cluster): CpfpInfo {
@@ -93,7 +95,7 @@ class TransactionRepository {
const ancestors: Ancestor[] = []; const ancestors: Ancestor[] = [];
let matched = false; let matched = false;
for (const tx of (cluster?.txs || [])) { for (const tx of cluster.txs) {
if (tx.txid === txid) { if (tx.txid === txid) {
matched = true; matched = true;
} else if (!matched) { } else if (!matched) {

View File

@@ -88,7 +88,5 @@ module.exports = {
verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+ verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+
walletLock: 'walletlock', walletLock: 'walletlock',
walletPassphrase: 'walletpassphrase', walletPassphrase: 'walletpassphrase',
walletPassphraseChange: 'walletpassphrasechange', walletPassphraseChange: 'walletpassphrasechange'
getTxoutSetinfo: 'gettxoutsetinfo', }
getIndexInfo: 'getindexinfo',
};

View File

@@ -72,7 +72,7 @@ class NetworkSyncService {
const graphNodesPubkeys: string[] = []; const graphNodesPubkeys: string[] = [];
for (const node of nodes) { for (const node of nodes) {
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key); const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
node.last_update = Math.max(node.last_update ?? 0, latestUpdated); node.last_update = Math.max(node.last_update, latestUpdated);
await nodesApi.$saveNode(node); await nodesApi.$saveNode(node);
graphNodesPubkeys.push(node.pub_key); graphNodesPubkeys.push(node.pub_key);
@@ -210,9 +210,6 @@ class NetworkSyncService {
const channels = await channelsApi.$getChannelsWithoutCreatedDate(); const channels = await channelsApi.$getChannelsWithoutCreatedDate();
for (const channel of channels) { for (const channel of channels) {
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id); const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
if (!transaction) {
continue;
}
await DB.query(` await DB.query(`
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
[transaction.timestamp, channel.id] [transaction.timestamp, channel.id]

View File

@@ -71,7 +71,7 @@ class FundingTxFetcher {
this.running = false; this.running = false;
} }
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number} | null> { public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
channelId = Common.channelIntegerIdToShortId(channelId); channelId = Common.channelIntegerIdToShortId(channelId);
if (this.fundingTxCache[channelId]) { if (this.fundingTxCache[channelId]) {
@@ -102,11 +102,6 @@ class FundingTxFetcher {
const rawTx = await bitcoinClient.getRawTransaction(txid); const rawTx = await bitcoinClient.getRawTransaction(txid);
const tx = await bitcoinClient.decodeRawTransaction(rawTx); const tx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!tx || !tx.vout || tx.vout.length < parseInt(outputIdx, 10) + 1 || tx.vout[outputIdx].value === undefined) {
logger.err(`Cannot find blockchain funding tx for channel id ${channelId}. Possible reasons are: bitcoin backend timeout or the channel shortId is not valid`);
return null;
}
this.fundingTxCache[channelId] = { this.fundingTxCache[channelId] = {
timestamp: block.time, timestamp: block.time,
txid: txid, txid: txid,

View File

@@ -411,7 +411,7 @@ class LightningStatsImporter {
} }
if (totalProcessed > 0) { if (totalProcessed > 0) {
logger.info(`Lightning network stats historical import completed`, logger.tags.ln); logger.notice(`Lightning network stats historical import completed`, logger.tags.ln);
} }
} catch (e) { } catch (e) {
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln); logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);

View File

@@ -8,18 +8,16 @@ import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https'; import * as https from 'https';
/** /**
* Maintain the most recent version of pools-v2.json * Maintain the most recent version of pools.json
*/ */
class PoolsUpdater { class PoolsUpdater {
lastRun: number = 0; lastRun: number = 0;
currentSha: string | null = null; currentSha: string | undefined = undefined;
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
public async updatePoolsJson(): Promise<void> { public async updatePoolsJson(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
config.MEMPOOL.ENABLED === false
) {
return; return;
} }
@@ -33,9 +31,15 @@ class PoolsUpdater {
this.lastRun = now; this.lastRun = now;
if (config.SOCKS5PROXY.ENABLED) {
logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`, logger.tags.mining);
} else {
logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`, logger.tags.mining);
}
try { try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
if (githubSha === null) { if (githubSha === undefined) {
return; return;
} }
@@ -43,57 +47,32 @@ class PoolsUpdater {
this.currentSha = await this.getShaFromDb(); this.currentSha = await this.getShaFromDb();
} }
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== null && this.currentSha === githubSha) { if (this.currentSha !== undefined && this.currentSha === githubSha) {
return; return;
} }
// See backend README for more details about the mining pools update process if (this.currentSha === undefined) {
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`, logger.tags.mining);
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
!process.env.npm_config_update_pools // We're not manually updating mining pool
) {
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_BLOCK_REINDEXING is disabled`);
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
return;
}
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
if (this.currentSha === null) {
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, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`, logger.tags.mining);
} }
const poolsJson = await this.query(this.poolsUrl); const poolsJson = await this.query(this.poolsUrl);
if (poolsJson === undefined) { if (poolsJson === undefined) {
return; return;
} }
poolsParser.setMiningPools(poolsJson); await poolsParser.migratePoolsJson(poolsJson);
await this.updateDBSha(githubSha);
if (config.DATABASE.ENABLED === false) { // Don't run db operations logger.notice(`PoolsUpdater completed`, logger.tags.mining);
logger.info('Mining pools-v2.json import completed (no database)');
return;
}
try {
await DB.query('START TRANSACTION;');
await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await DB.query('COMMIT;');
} catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
await DB.query('ROLLBACK;');
}
logger.info('PoolsUpdater completed');
} catch (e) { } catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); logger.err(`PoolsUpdater failed. Will try again in 24h. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools.json sha from the db
*/ */
private async updateDBSha(githubSha: string): Promise<void> { private async updateDBSha(githubSha: string): Promise<void> {
this.currentSha = githubSha; this.currentSha = githubSha;
@@ -102,46 +81,46 @@ class PoolsUpdater {
await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) { } catch (e) {
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
} }
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from the db * Fetch our latest pools.json sha from the db
*/ */
private async getShaFromDb(): Promise<string | null> { private async getShaFromDb(): Promise<string | undefined> {
try { try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null); return (rows.length > 0 ? rows[0].string : undefined);
} catch (e) { } catch (e) {
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
return null; return undefined;
} }
} }
/** /**
* Fetch our latest pools-v2.json sha from github * Fetch our latest pools.json sha from github
*/ */
private async fetchPoolsSha(): Promise<string | null> { private async fetchPoolsSha(): Promise<string | undefined> {
const response = await this.query(this.treeUrl); const response = await this.query(this.treeUrl);
if (response !== undefined) { if (response !== undefined) {
for (const file of response['tree']) { for (const file of response['tree']) {
if (file['path'] === 'pools-v2.json') { if (file['path'] === 'pools.json') {
return file['sha']; return file['sha'];
} }
} }
} }
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`, logger.tags.mining);
return null; return undefined;
} }
/** /**
* Http request wrapper * Http request wrapper
*/ */
private async query(path): Promise<any[] | undefined> { private async query(path): Promise<object | undefined> {
type axiosOptions = { type axiosOptions = {
headers: { headers: {
'User-Agent': string 'User-Agent': string

View File

@@ -8,13 +8,12 @@ class BitfinexApi implements PriceFeed {
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC'; public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist'; public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
constructor() {
}
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['last_price']) { return response ? parseInt(response['last_price'], 10) : -1;
return parseInt(response['last_price'], 10);
} else {
return -1;
}
} }
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> { public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {

View File

@@ -13,11 +13,7 @@ class BitflyerApi implements PriceFeed {
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['ltp']) { return response ? parseInt(response['ltp'], 10) : -1;
return parseInt(response['ltp'], 10);
} else {
return -1;
}
} }
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> { public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {

View File

@@ -13,11 +13,7 @@ class CoinbaseApi implements PriceFeed {
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['data'] && response['data']['amount']) { return response ? parseInt(response['data']['amount'], 10) : -1;
return parseInt(response['data']['amount'], 10);
} else {
return -1;
}
} }
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> { public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {

View File

@@ -13,11 +13,7 @@ class GeminiApi implements PriceFeed {
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
if (response && response['last']) { return response ? parseInt(response['last'], 10) : -1;
return parseInt(response['last'], 10);
} else {
return -1;
}
} }
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> { public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {

View File

@@ -23,14 +23,7 @@ class KrakenApi implements PriceFeed {
public async $fetchPrice(currency): Promise<number> { public async $fetchPrice(currency): Promise<number> {
const response = await query(this.url + currency); const response = await query(this.url + currency);
const ticker = this.getTicker(currency); return response ? parseInt(response['result'][this.getTicker(currency)]['c'][0], 10) : -1;
if (response && response['result'] && response['result'][ticker] &&
response['result'][ticker]['c'] && response['result'][ticker]['c'].length > 0
) {
return parseInt(response['result'][ticker]['c'][0], 10);
} else {
return -1;
}
} }
public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> { public async $fetchRecentPrice(currencies: string[], type: 'hour' | 'day'): Promise<PriceHistory> {
@@ -98,7 +91,7 @@ class KrakenApi implements PriceFeed {
} }
if (Object.keys(priceHistory).length > 0) { if (Object.keys(priceHistory).length > 0) {
logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining); logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
} }
} }
} }

View File

@@ -1,8 +1,8 @@
import * as fs from 'fs'; import * as fs from 'fs';
import path from 'path'; import path from "path";
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository'; import PricesRepository from '../repositories/PricesRepository';
import BitfinexApi from './price-feeds/bitfinex-api'; import BitfinexApi from './price-feeds/bitfinex-api';
import BitflyerApi from './price-feeds/bitflyer-api'; import BitflyerApi from './price-feeds/bitflyer-api';
import CoinbaseApi from './price-feeds/coinbase-api'; import CoinbaseApi from './price-feeds/coinbase-api';
@@ -20,18 +20,27 @@ export interface PriceFeed {
} }
export interface PriceHistory { export interface PriceHistory {
[timestamp: number]: ApiPrice; [timestamp: number]: Prices;
}
export interface Prices {
USD: number;
EUR: number;
GBP: number;
CAD: number;
CHF: number;
AUD: number;
JPY: number;
} }
class PriceUpdater { class PriceUpdater {
public historyInserted = false; public historyInserted = false;
private lastRun = 0; lastRun = 0;
private lastHistoricalRun = 0; lastHistoricalRun = 0;
private running = false; running = false;
private feeds: PriceFeed[] = []; feeds: PriceFeed[] = [];
private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY']; currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
private latestPrices: ApiPrice; latestPrices: Prices;
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
constructor() { constructor() {
this.latestPrices = this.getEmptyPricesObj(); this.latestPrices = this.getEmptyPricesObj();
@@ -43,13 +52,8 @@ class PriceUpdater {
this.feeds.push(new GeminiApi()); this.feeds.push(new GeminiApi());
} }
public getLatestPrices(): ApiPrice { public getEmptyPricesObj(): Prices {
return this.latestPrices;
}
public getEmptyPricesObj(): ApiPrice {
return { return {
time: 0,
USD: -1, USD: -1,
EUR: -1, EUR: -1,
GBP: -1, GBP: -1,
@@ -60,24 +64,7 @@ class PriceUpdater {
}; };
} }
public setRatesChangedCallback(fn: (rates: ApiPrice) => void): void {
this.ratesChangedCallback = fn;
}
/**
* We execute this function before the websocket initialization since
* the websocket init is not done asyncronously
*/
public async $initializeLatestPriceWithDb(): Promise<void> {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
}
public async $run(): Promise<void> { public async $run(): Promise<void> {
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
// Coins have no value on testnet/signet, so we want to always show 0
return;
}
if (this.running === true) { if (this.running === true) {
return; return;
} }
@@ -89,11 +76,12 @@ class PriceUpdater {
} }
try { try {
await this.$updatePrice();
if (this.historyInserted === false && config.DATABASE.ENABLED === true) { if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
await this.$insertHistoricalPrices(); await this.$insertHistoricalPrices();
} else {
await this.$updatePrice();
} }
} catch (e: any) { } catch (e) {
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining); logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
} }
@@ -124,7 +112,7 @@ class PriceUpdater {
if (feed.currencies.includes(currency)) { if (feed.currencies.includes(currency)) {
try { try {
const price = await feed.$fetchPrice(currency); const price = await feed.$fetchPrice(currency);
if (price > -1 && price < MAX_PRICES[currency]) { if (price > 0) {
prices.push(price); prices.push(price);
} }
logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining); logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining);
@@ -139,11 +127,7 @@ class PriceUpdater {
// Compute average price, non weighted // Compute average price, non weighted
prices = prices.filter(price => price > 0); prices = prices.filter(price => price > 0);
if (prices.length === 0) { this.latestPrices[currency] = Math.round((prices.reduce((partialSum, a) => partialSum + a, 0)) / prices.length);
this.latestPrices[currency] = -1;
} else {
this.latestPrices[currency] = Math.round((prices.reduce((partialSum, a) => partialSum + a, 0)) / prices.length);
}
} }
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`); logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
@@ -160,15 +144,7 @@ class PriceUpdater {
} }
} }
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.latestPrices);
}
this.lastRun = new Date().getTime() / 1000; this.lastRun = new Date().getTime() / 1000;
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
}
} }
/** /**
@@ -237,7 +213,7 @@ class PriceUpdater {
// Group them by timestamp and currency, for example // Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4]; // grouped[123456789]['USD'] = [1, 2, 3, 4];
const grouped = {}; const grouped: Object = {};
for (const historicalEntry of historicalPrices) { for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) { for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) { if (existingPriceTimes.includes(parseInt(time, 10))) {
@@ -252,8 +228,8 @@ class PriceUpdater {
for (const currency of this.currencies) { for (const currency of this.currencies) {
const price = historicalEntry[time][currency]; const price = historicalEntry[time][currency];
if (price > -1 && price < MAX_PRICES[currency]) { if (price > 0) {
grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price); grouped[time][currency].push(parseInt(price, 10));
} }
} }
} }
@@ -262,7 +238,7 @@ class PriceUpdater {
// Average prices and insert everything into the db // Average prices and insert everything into the db
let totalInserted = 0; let totalInserted = 0;
for (const time in grouped) { for (const time in grouped) {
const prices: ApiPrice = this.getEmptyPricesObj(); const prices: Prices = this.getEmptyPricesObj();
for (const currency in grouped[time]) { for (const currency in grouped[time]) {
if (grouped[time][currency].length === 0) { if (grouped[time][currency].length === 0) {
continue; continue;

View File

@@ -0,0 +1,33 @@
import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{
id: block.id ?? block.hash, // hash for indexed block
timestamp: block.timestamp ?? block.time ?? block.blockTimestamp, // blockTimestamp for indexed block
height: block.height,
version: block.version,
bits: (typeof block.bits === 'string' ? parseInt(block.bits, 16): block.bits),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkle_root ?? block.merkleroot,
tx_count: block.tx_count ?? block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
extras: {
coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw,
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
feeRange: block.feeRange ?? block.fee_span,
reward: block.reward ?? block?.extras?.reward,
totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
avgFee: block?.extras?.avgFee ?? block.avg_fee,
avgFeeRate: block?.avgFeeRate ?? block.avg_fee_rate,
pool: block?.extras?.pool ?? (block?.pool_id ? {
id: block.pool_id,
name: block.pool_name,
slug: block.pool_slug,
} : undefined),
usd: block?.extras?.usd ?? block.usd ?? null,
}
};
}

View File

@@ -1,14 +0,0 @@
// simple recursive deep clone for literal-type objects
// does not preserve Dates, Maps, Sets etc
// does not support recursive objects
// properties deeper than maxDepth will be shallow cloned
export function deepClone(obj: any, maxDepth: number = 50, depth: number = 0): any {
let cloned = obj;
if (depth < maxDepth && typeof obj === 'object') {
cloned = Array.isArray(obj) ? [] : {};
for (const key in obj) {
cloned[key] = deepClone(obj[key], maxDepth, depth + 1);
}
}
return cloned;
}

View File

@@ -1,29 +0,0 @@
const byteUnits = ['B', 'kB', 'MB', 'GB', 'TB'];
export function getBytesUnit(bytes: number): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return 'B';
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && bytes > 1024) {
unitIndex++;
bytes /= 1024;
}
return byteUnits[unitIndex];
}
export function formatBytes(bytes: number, toUnit: string, skipUnit = false): string {
if (isNaN(bytes) || !isFinite(bytes)) {
return `${bytes}`;
}
let unitIndex = 0;
while (unitIndex < byteUnits.length && (toUnit && byteUnits[unitIndex] !== toUnit || (!toUnit && bytes > 1024))) {
unitIndex++;
bytes /= 1024;
}
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
}

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: AlexLloyd0

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
Signed: Arooba-git

View File

@@ -1,3 +0,0 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 17, 2022.
Signed: piterden

View File

@@ -17,7 +17,7 @@ _Note: address lookups require an Electrum Server and will not work with this co
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file: The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
```ini ```
txindex=1 txindex=1
server=1 server=1
rpcuser=mempool rpcuser=mempool
@@ -26,7 +26,7 @@ rpcpassword=mempool
If you want to use different credentials, specify them in the `docker-compose.yml` file: If you want to use different credentials, specify them in the `docker-compose.yml` file:
```yaml ```
api: api:
environment: environment:
MEMPOOL_BACKEND: "none" MEMPOOL_BACKEND: "none"
@@ -54,7 +54,7 @@ First, configure `bitcoind` as specified above, and make sure your Electrum Serv
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server: Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
```yaml ```
api: api:
environment: environment:
MEMPOOL_BACKEND: "electrum" MEMPOOL_BACKEND: "electrum"
@@ -85,7 +85,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"MEMPOOL": { "MEMPOOL": {
"NETWORK": "mainnet", "NETWORK": "mainnet",
"BACKEND": "electrum", "BACKEND": "electrum",
@@ -101,22 +101,22 @@ Below we list all settings from `mempool-config.json` and the corresponding over
"INITIAL_BLOCKS_AMOUNT": 8, "INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8, "MEMPOOL_BLOCKS_AMOUNT": 8,
"BLOCKS_SUMMARIES_INDEXING": false, "BLOCKS_SUMMARIES_INDEXING": false,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false, "USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [], "EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"],
"STDOUT_LOG_MIN_PRIORITY": "info", "STDOUT_LOG_MIN_PRIORITY": "info",
"INDEXING_BLOCKS_AMOUNT": false, "INDEXING_BLOCKS_AMOUNT": false,
"AUTOMATIC_BLOCK_REINDEXING": false, "AUTOMATIC_BLOCK_REINDEXING": false,
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"ADVANCED_GBT_AUDIT": false, "ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false, "ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false, "CPFP_INDEXING": false,
"MAX_BLOCKS_BULK_QUERY": 0,
}, },
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
MEMPOOL_NETWORK: "" MEMPOOL_NETWORK: ""
@@ -132,6 +132,7 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_INITIAL_BLOCKS_AMOUNT: "" MEMPOOL_INITIAL_BLOCKS_AMOUNT: ""
MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: "" MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: ""
MEMPOOL_BLOCKS_SUMMARIES_INDEXING: "" MEMPOOL_BLOCKS_SUMMARIES_INDEXING: ""
MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: ""
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: "" MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
MEMPOOL_EXTERNAL_ASSETS: "" MEMPOOL_EXTERNAL_ASSETS: ""
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: "" MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
@@ -142,7 +143,6 @@ Corresponding `docker-compose.yml` overrides:
MEMPOOL_ADVANCED_GBT_AUDIT: "" MEMPOOL_ADVANCED_GBT_AUDIT: ""
MEMPOOL_ADVANCED_GBT_MEMPOOL: "" MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
MEMPOOL_CPFP_INDEXING: "" MEMPOOL_CPFP_INDEXING: ""
MAX_BLOCKS_BULK_QUERY: ""
... ...
``` ```
@@ -153,8 +153,8 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
"USERNAME": "mempool", "USERNAME": "mempool",
@@ -163,7 +163,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
CORE_RPC_HOST: "" CORE_RPC_HOST: ""
@@ -176,7 +176,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 50002, "PORT": 50002,
@@ -185,7 +185,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
ELECTRUM_HOST: "" ELECTRUM_HOST: ""
@@ -197,14 +197,14 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000" "REST_API_URL": "http://127.0.0.1:3000"
}, },
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
ESPLORA_REST_API_URL: "" ESPLORA_REST_API_URL: ""
@@ -214,7 +214,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"SECOND_CORE_RPC": { "SECOND_CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 8332, "PORT": 8332,
@@ -224,7 +224,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
SECOND_CORE_RPC_HOST: "" SECOND_CORE_RPC_HOST: ""
@@ -237,7 +237,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"DATABASE": { "DATABASE": {
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -249,7 +249,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
DATABASE_ENABLED: "" DATABASE_ENABLED: ""
@@ -264,7 +264,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"SYSLOG": { "SYSLOG": {
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -275,7 +275,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
SYSLOG_ENABLED: "" SYSLOG_ENABLED: ""
@@ -289,7 +289,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"STATISTICS": { "STATISTICS": {
"ENABLED": true, "ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150 "TX_PER_SECOND_SAMPLE_PERIOD": 150
@@ -297,7 +297,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
STATISTICS_ENABLED: "" STATISTICS_ENABLED: ""
@@ -308,7 +308,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"BISQ": { "BISQ": {
"ENABLED": false, "ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db" "DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
@@ -316,7 +316,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
BISQ_ENABLED: "" BISQ_ENABLED: ""
@@ -327,7 +327,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"SOCKS5PROXY": { "SOCKS5PROXY": {
"ENABLED": false, "ENABLED": false,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
@@ -338,7 +338,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
SOCKS5PROXY_ENABLED: "" SOCKS5PROXY_ENABLED: ""
@@ -352,7 +352,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"PRICE_DATA_SERVER": { "PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices", "TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices" "CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
@@ -360,7 +360,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
PRICE_DATA_SERVER_TOR_URL: "" PRICE_DATA_SERVER_TOR_URL: ""
@@ -371,7 +371,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"LIGHTNING": { "LIGHTNING": {
"ENABLED": false "ENABLED": false
"BACKEND": "lnd" "BACKEND": "lnd"
@@ -383,7 +383,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
LIGHTNING_ENABLED: false LIGHTNING_ENABLED: false
@@ -398,7 +398,7 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"LND": { "LND": {
"TLS_CERT_PATH": "" "TLS_CERT_PATH": ""
"MACAROON_PATH": "" "MACAROON_PATH": ""
@@ -407,7 +407,7 @@ Corresponding `docker-compose.yml` overrides:
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
LND_TLS_CERT_PATH: "" LND_TLS_CERT_PATH: ""
@@ -419,39 +419,16 @@ Corresponding `docker-compose.yml` overrides:
<br/> <br/>
`mempool-config.json`: `mempool-config.json`:
```json ```
"CLIGHTNING": { "CLIGHTNING": {
"SOCKET": "" "SOCKET": ""
} }
``` ```
Corresponding `docker-compose.yml` overrides: Corresponding `docker-compose.yml` overrides:
```yaml ```
api: api:
environment: environment:
CLIGHTNING_SOCKET: "" CLIGHTNING_SOCKET: ""
... ...
``` ```
<br/>
`mempool-config.json`:
```json
"MAXMIND": {
"ENABLED": true,
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
}
```
Corresponding `docker-compose.yml` overrides:
```yaml
api:
environment:
MAXMIND_ENABLED: true,
MAXMIND_GEOLITE2_CITY: "./backend/GeoIP/GeoLite2-City.mmdb",
MAXMIND_GEOLITE2_ASN": "./backend/GeoIP/GeoLite2-ASN.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

@@ -13,6 +13,7 @@
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__, "BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
"INITIAL_BLOCKS_AMOUNT": __MEMPOOL_INITIAL_BLOCKS_AMOUNT__, "INITIAL_BLOCKS_AMOUNT": __MEMPOOL_INITIAL_BLOCKS_AMOUNT__,
"MEMPOOL_BLOCKS_AMOUNT": __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__, "MEMPOOL_BLOCKS_AMOUNT": __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__,
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__, "USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__, "EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
"EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__, "EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__,
@@ -22,11 +23,10 @@
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__, "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__, "AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"AUDIT": __MEMPOOL_AUDIT__,
"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__ "RBF_DUAL_NODE": __RBF_DUAL_NODE__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",
@@ -107,11 +107,5 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__", "LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__", "BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__" "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"MAXMIND": {
"ENABLED": __MAXMIND_ENABLED__,
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
} }
} }

View File

@@ -16,20 +16,22 @@ __MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8} __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000} __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__=${MEMPOOL_BLOCKS_SUMMARIES_INDEXING:=false} __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__=${MEMPOOL_BLOCKS_SUMMARIES_INDEXING:=false}
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false} __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]} __MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1} __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false} __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_RBF_DUAL_NODE__=${MEMPOOL_RBF_DUAL_NODE:=false}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -111,13 +113,6 @@ __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
# CLN # CLN
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""} __CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
# MAXMIND
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="./backend/GeoIP/GeoLite2-City.mmdb"}
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="./backend/GeoIP/GeoLite2-ASN.mmdb"}
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@@ -135,20 +130,21 @@ sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_BLOCKS_AMOUNT__}
sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__/${__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json sed -i "s/__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__/${__MEMPOOL_BLOCKS_SUMMARIES_INDEXING__}/g" mempool-config.json
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
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_RBF_DUAL_NODE__/${__MEMPOOL_RBF_DUAL_NODE__}/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
@@ -220,11 +216,4 @@ sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!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
# MAXMIND
sed -i "s!__MAXMIND_ENABLED__!${__MAXMIND_ENABLED__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View File

@@ -31,11 +31,9 @@ __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets} __BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true} __MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
__LIGHTNING__=${LIGHTNING:=false} __LIGHTNING__=${LIGHTNING:=false}
__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}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
# Export as environment variables to be used by envsubst # Export as environment variables to be used by envsubst
export __TESTNET_ENABLED__ export __TESTNET_ENABLED__
@@ -57,11 +55,9 @@ export __LIQUID_WEBSITE_URL__
export __BISQ_WEBSITE_URL__ export __BISQ_WEBSITE_URL__
export __MINING_DASHBOARD__ export __MINING_DASHBOARD__
export __LIGHTNING__ export __LIGHTNING__
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 __HISTORICAL_PRICE__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname) folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder} echo ${folder}

View File

@@ -3,11 +3,6 @@
#backend #backend
cp ./docker/backend/* ./backend/ cp ./docker/backend/* ./backend/
#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-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb
#frontend #frontend
localhostIP="127.0.0.1" localhostIP="127.0.0.1"
cp ./docker/frontend/* ./frontend cp ./docker/frontend/* ./frontend

2
frontend/.gitignore vendored
View File

@@ -54,8 +54,6 @@ src/resources/assets-testnet.json
src/resources/assets-testnet.minimal.json src/resources/assets-testnet.minimal.json
src/resources/pools.json src/resources/pools.json
src/resources/mining-pools/* src/resources/mining-pools/*
src/resources/**/*.mp4
src/resources/**/*.vtt
# environment config # environment config
mempool-frontend-config.json mempool-frontend-config.json

View File

@@ -1,9 +1,7 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
[o:mempool:p:mempool:r:frontend-src-locale-messages-xlf--master] [mempool.frontend-src-locale-messages-xlf--master]
file_filter = frontend/src/locale/messages.<lang>.xlf file_filter = frontend/src/locale/messages.<lang>.xlf
source_file = frontend/src/locale/messages.en-US.xlf
source_lang = en-US source_lang = en-US
type = XLIFF type = XLIFF

View File

@@ -111,7 +111,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Spanish @maxhodler @bisqes * Spanish @maxhodler @bisqes
* Persian @techmix * Persian @techmix
* French @Bayernatoor * French @Bayernatoor
* Korean @kcalvinalvinn @sogoagain * Korean @kcalvinalvinn
* Italian @HodlBits * Italian @HodlBits
* Hebrew @rapidlab309 * Hebrew @rapidlab309
* Georgian @wyd_idk * Georgian @wyd_idk
@@ -132,4 +132,3 @@ https://www.transifex.com/mempool/mempool/dashboard/
* Russian @TonyCrusoe @Bitconan * Russian @TonyCrusoe @Bitconan
* Romanian @mirceavesa * Romanian @mirceavesa
* Macedonian @SkechBoy * Macedonian @SkechBoy
* Nepalese @kebinm

View File

@@ -38,10 +38,6 @@
"translation": "src/locale/messages.de.xlf", "translation": "src/locale/messages.de.xlf",
"baseHref": "/de/" "baseHref": "/de/"
}, },
"da": {
"translation": "src/locale/messages.da.xlf",
"baseHref": "/da/"
},
"es": { "es": {
"translation": "src/locale/messages.es.xlf", "translation": "src/locale/messages.es.xlf",
"baseHref": "/es/" "baseHref": "/es/"
@@ -142,10 +138,6 @@
"translation": "src/locale/messages.hi.xlf", "translation": "src/locale/messages.hi.xlf",
"baseHref": "/hi/" "baseHref": "/hi/"
}, },
"ne": {
"translation": "src/locale/messages.ne.xlf",
"baseHref": "/ne/"
},
"lt": { "lt": {
"translation": "src/locale/messages.lt.xlf", "translation": "src/locale/messages.lt.xlf",
"baseHref": "/lt/" "baseHref": "/lt/"

View File

@@ -1,4 +1,4 @@
import { defineConfig } from 'cypress'; import { defineConfig } from 'cypress'
export default defineConfig({ export default defineConfig({
projectId: 'ry4br7', projectId: 'ry4br7',
@@ -12,18 +12,12 @@ export default defineConfig({
}, },
chromeWebSecurity: false, chromeWebSecurity: false,
e2e: { e2e: {
setupNodeEvents(on: any, config: any) { // We've imported your old cypress plugins here.
const fs = require('fs'); // You may want to clean this up later by importing these.
const CONFIG_FILE = 'mempool-frontend-config.json'; setupNodeEvents(on, config) {
if (fs.existsSync(CONFIG_FILE)) { return require('./cypress/plugins/index.js')(on, config)
let contents = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
config.env.BASE_MODULE = contents.BASE_MODULE ? contents.BASE_MODULE : 'mempool';
} else {
config.env.BASE_MODULE = 'mempool';
}
return config;
}, },
baseUrl: 'http://localhost:4200', baseUrl: 'http://localhost:4200',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
}, },
}); })

View File

@@ -1,5 +1,5 @@
describe('Bisq', () => { describe('Bisq', () => {
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
const basePath = ''; const basePath = '';
beforeEach(() => { beforeEach(() => {
@@ -20,7 +20,7 @@ describe('Bisq', () => {
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
}); });
describe('transactions', () => { describe("transactions", () => {
it('loads the transactions screen', () => { it('loads the transactions screen', () => {
cy.visit(`${basePath}`); cy.visit(`${basePath}`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
@@ -30,9 +30,9 @@ describe('Bisq', () => {
}); });
const filters = [ const filters = [
'Asset listing fee', 'Blind vote', 'Compensation request', "Asset listing fee", "Blind vote", "Compensation request",
'Genesis', 'Irregular', 'Lockup', 'Pay trade fee', 'Proof of burn', "Genesis", "Irregular", "Lockup", "Pay trade fee", "Proof of burn",
'Proposal', 'Reimbursement request', 'Transfer BSQ', 'Unlock', 'Vote reveal' "Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
]; ];
filters.forEach((filter) => { filters.forEach((filter) => {
it.only(`filters the transaction screen by ${filter}`, () => { it.only(`filters the transaction screen by ${filter}`, () => {
@@ -49,7 +49,7 @@ describe('Bisq', () => {
}); });
}); });
it('filters using multiple criteria', () => { it("filters using multiple criteria", () => {
const filters = ['Proposal', 'Lockup', 'Unlock']; const filters = ['Proposal', 'Lockup', 'Unlock'];
cy.visit(`${basePath}/transactions`); cy.visit(`${basePath}/transactions`);
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();

View File

@@ -1,5 +1,5 @@
describe('Liquid', () => { describe('Liquid', () => {
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
const basePath = ''; const basePath = '';
beforeEach(() => { beforeEach(() => {

View File

@@ -1,5 +1,5 @@
describe('Liquid Testnet', () => { describe('Liquid Testnet', () => {
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
const basePath = '/testnet'; const basePath = '/testnet';
beforeEach(() => { beforeEach(() => {

View File

@@ -1,6 +1,6 @@
import { emitMempoolInfo, dropWebSocket } from '../../support/websocket'; import { emitMempoolInfo, dropWebSocket } from "../../support/websocket";
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
//Credit: https://github.com/bahmutov/cypress-examples/blob/6cedb17f83a3bb03ded13cf1d6a3f0656ca2cdf5/docs/recipes/overlapping-elements.md //Credit: https://github.com/bahmutov/cypress-examples/blob/6cedb17f83a3bb03ded13cf1d6a3f0656ca2cdf5/docs/recipes/overlapping-elements.md
@@ -64,7 +64,7 @@ describe('Mainnet', () => {
it('loads the status screen', () => { it('loads the status screen', () => {
cy.visit('/status'); cy.visit('/status');
cy.get('#mempool-block-0').should('be.visible'); cy.get('#mempool-block-0').should('be.visible');
cy.get('[id^="bitcoin-block-"]').should('have.length', 22); cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
cy.get('.footer').should('be.visible'); cy.get('.footer').should('be.visible');
cy.get('.row > :nth-child(1)').invoke('text').then((text) => { cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
expect(text).to.match(/Incoming transactions.* vB\/s/); expect(text).to.match(/Incoming transactions.* vB\/s/);
@@ -219,11 +219,11 @@ describe('Mainnet', () => {
describe('blocks navigation', () => { describe('blocks navigation', () => {
describe('keyboard events', () => { describe('keyboard events', () => {
it('loads first blockchain block visible and keypress arrow right', () => { it('loads first blockchain blocks visible and keypress arrow right', () => {
cy.viewport('macbook-16'); cy.viewport('macbook-16');
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('[data-cy="bitcoin-block-offset-0-index-0"]').click().then(() => { cy.get('.blockchain-blocks-0 > a').click().then(() => {
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist'); cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.waitForPageIdle(); cy.waitForPageIdle();
@@ -233,11 +233,11 @@ describe('Mainnet', () => {
}); });
}); });
it('loads first blockchain block visible and keypress arrow left', () => { it('loads first blockchain blocks visible and keypress arrow left', () => {
cy.viewport('macbook-16'); cy.viewport('macbook-16');
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('[data-cy="bitcoin-block-offset-0-index-0"]').click().then(() => { cy.get('.blockchain-blocks-0 > a').click().then(() => {
cy.waitForPageIdle(); cy.waitForPageIdle();
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist'); cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
@@ -246,11 +246,11 @@ describe('Mainnet', () => {
}); });
}); });
it.skip('loads last blockchain block and keypress arrow right', () => { //Skip for now as "last" doesn't really work with infinite scrolling it('loads last blockchain blocks and keypress arrow right', () => {
cy.viewport('macbook-16'); cy.viewport('macbook-16');
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('bitcoin-block-offset-0-index-7').click().then(() => { cy.get('.blockchain-blocks-4 > a').click().then(() => {
cy.waitForPageIdle(); cy.waitForPageIdle();
// block 6 // block 6
@@ -309,7 +309,7 @@ describe('Mainnet', () => {
cy.viewport('macbook-16'); cy.viewport('macbook-16');
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('[data-cy="bitcoin-block-offset-0-index-0"]').click().then(() => { cy.get('.blockchain-blocks-0 > a').click().then(() => {
cy.waitForPageIdle(); cy.waitForPageIdle();
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist'); cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible'); cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
@@ -339,14 +339,14 @@ describe('Mainnet', () => {
cy.visit('/'); cy.visit('/');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.changeNetwork('testnet'); cy.changeNetwork("testnet");
cy.changeNetwork('signet'); cy.changeNetwork("signet");
cy.changeNetwork('mainnet'); cy.changeNetwork("mainnet");
}); });
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit('/'); cy.visit("/");
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');

View File

@@ -1,4 +1,4 @@
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
describe('Mainnet - Mining Features', () => { describe('Mainnet - Mining Features', () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -1,6 +1,6 @@
import { emitMempoolInfo } from '../../support/websocket'; import { emitMempoolInfo } from "../../support/websocket";
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
describe('Signet', () => { describe('Signet', () => {
beforeEach(() => { beforeEach(() => {
@@ -25,7 +25,7 @@ describe('Signet', () => {
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit('/signet'); cy.visit("/signet");
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');
@@ -35,7 +35,7 @@ describe('Signet', () => {
emitMempoolInfo({ emitMempoolInfo({
'params': { 'params': {
'network': 'signet' "network": "signet"
} }
}); });

View File

@@ -1,6 +1,6 @@
import { emitMempoolInfo } from '../../support/websocket'; import { confirmAddress, emitMempoolInfo, sendWsMock, showNewTx, startTrackingAddress } from "../../support/websocket";
const baseModule = Cypress.env('BASE_MODULE'); const baseModule = Cypress.env("BASE_MODULE");
describe('Testnet', () => { describe('Testnet', () => {
beforeEach(() => { beforeEach(() => {
@@ -25,7 +25,7 @@ describe('Testnet', () => {
it.skip('loads the dashboard with the skeleton blocks', () => { it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket(); cy.mockMempoolSocket();
cy.visit('/testnet'); cy.visit("/testnet");
cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(1) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(2) > #bitcoin-block-0').should('be.visible');
cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible'); cy.get(':nth-child(3) > #bitcoin-block-0').should('be.visible');

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
const CONFIG_FILE = 'mempool-frontend-config.json';
module.exports = (on, config) => {
if (fs.existsSync(CONFIG_FILE)) {
let contents = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
config.env.BASE_MODULE = contents.BASE_MODULE ? contents.BASE_MODULE : 'mempool';
} else {
config.env.BASE_MODULE = 'mempool';
}
return config;
}

View File

@@ -17,10 +17,10 @@
"LIQUID_WEBSITE_URL": "https://liquid.network", "LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets", "BISQ_WEBSITE_URL": "https://bisq.markets",
"MINING_DASHBOARD": true, "MINING_DASHBOARD": true,
"AUDIT": false,
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0, "MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
"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,
"HISTORICAL_PRICE": true "FULL_RBF_ENABLED": false,
"ALT_BACKEND_ENABLED": false
} }

View File

@@ -58,7 +58,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.4.0", "@cypress/schematic": "^2.4.0",
"cypress": "^12.7.0", "cypress": "^12.3.0",
"cypress-fail-on-console-error": "~4.0.2", "cypress-fail-on-console-error": "~4.0.2",
"cypress-wait-until": "^1.7.2", "cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5", "mock-socket": "~9.1.5",
@@ -7010,9 +7010,9 @@
"peer": true "peer": true
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "12.7.0", "version": "12.3.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.7.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz",
"integrity": "sha512-7rq+nmhzz0u6yabCFyPtADU2OOrYt6pvUau9qV7xyifJ/hnsaw/vkr0tnLlcuuQKUAOC1v1M1e4Z0zG7S0IAvA==", "integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -7033,7 +7033,7 @@
"commander": "^5.1.0", "commander": "^5.1.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.4", "debug": "^4.3.2",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "6.4.7", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
@@ -7159,23 +7159,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/cypress/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/cypress/node_modules/execa": { "node_modules/cypress/node_modules/execa": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
@@ -22293,9 +22276,9 @@
"peer": true "peer": true
}, },
"cypress": { "cypress": {
"version": "12.7.0", "version": "12.3.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.7.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz",
"integrity": "sha512-7rq+nmhzz0u6yabCFyPtADU2OOrYt6pvUau9qV7xyifJ/hnsaw/vkr0tnLlcuuQKUAOC1v1M1e4Z0zG7S0IAvA==", "integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==",
"optional": true, "optional": true,
"requires": { "requires": {
"@cypress/request": "^2.88.10", "@cypress/request": "^2.88.10",
@@ -22315,7 +22298,7 @@
"commander": "^5.1.0", "commander": "^5.1.0",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.4", "debug": "^4.3.2",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "6.4.7", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
@@ -22399,15 +22382,6 @@
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"optional": true "optional": true
}, },
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"requires": {
"ms": "2.1.2"
}
},
"execa": { "execa": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",

View File

@@ -110,7 +110,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^2.4.0", "@cypress/schematic": "^2.4.0",
"cypress": "^12.7.0", "cypress": "^12.3.0",
"cypress-fail-on-console-error": "~4.0.2", "cypress-fail-on-console-error": "~4.0.2",
"cypress-wait-until": "^1.7.2", "cypress-wait-until": "^1.7.2",
"mock-socket": "~9.1.5", "mock-socket": "~9.1.5",
@@ -119,4 +119,4 @@
"scarfSettings": { "scarfSettings": {
"enabled": false "enabled": false
} }
} }

View File

@@ -3,8 +3,8 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf'); let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => { PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); entry.target = entry.target.replace("mempool.space", "mempool-staging.tk7.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); entry.target = entry.target.replace("liquid.network", "liquid-staging.tk7.mempool.space");
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space"); entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
}); });

View File

@@ -72,10 +72,22 @@ export const chartColors = [
]; ];
export const poolsColor = { export const poolsColor = {
'unknown': '#FDD835', 'foundryusa': '#D81B60',
}; 'antpool': '#8E24AA',
'f2pool': '#5E35B1',
'poolin': '#3949AB',
'binancepool': '#1E88E5',
'viabtc': '#039BE5',
'btccom': '#00897B',
'braiinspool': '#00ACC1',
'sbicrypto': '#43A047',
'marapool': '#7CB342',
'luxor': '#C0CA33',
'unknown': '#FDD835',
'okkong': '#FFB300',
}
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
export interface Language { export interface Language {
@@ -87,9 +99,9 @@ export const languages: Language[] = [
{ code: 'ar', name: 'العربية' }, // Arabic { code: 'ar', name: 'العربية' }, // Arabic
// { code: 'bg', name: 'Български' }, // Bulgarian // { code: 'bg', name: 'Български' }, // Bulgarian
// { code: 'bs', name: 'Bosanski' }, // Bosnian // { code: 'bs', name: 'Bosanski' }, // Bosnian
// { code: 'ca', name: 'Català' }, // Catalan { code: 'ca', name: 'Català' }, // Catalan
{ code: 'cs', name: 'Čeština' }, // Czech { code: 'cs', name: 'Čeština' }, // Czech
{ code: 'da', name: 'Dansk' }, // Danish // { code: 'da', name: 'Dansk' }, // Danish
{ code: 'de', name: 'Deutsch' }, // German { code: 'de', name: 'Deutsch' }, // German
// { code: 'et', name: 'Eesti' }, // Estonian // { code: 'et', name: 'Eesti' }, // Estonian
// { code: 'el', name: 'Ελληνικά' }, // Greek // { code: 'el', name: 'Ελληνικά' }, // Greek
@@ -104,7 +116,6 @@ export const languages: Language[] = [
// { code: 'hr', name: 'Hrvatski' }, // Croatian // { code: 'hr', name: 'Hrvatski' }, // Croatian
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
{ code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'hi', name: 'हिन्दी' }, // Hindi
{ code: 'ne', name: 'नेपाली' }, // Nepalese
{ code: 'it', name: 'Italiano' }, // Italian { code: 'it', name: 'Italiano' }, // Italian
{ code: 'he', name: 'עברית' }, // Hebrew { code: 'he', name: 'עברית' }, // Hebrew
{ code: 'ka', name: 'ქართული' }, // Georgian { code: 'ka', name: 'ქართული' }, // Georgian
@@ -136,127 +147,12 @@ export const languages: Language[] = [
]; ];
export const specialBlocks = { export const specialBlocks = {
'0': {
labelEvent: 'Genesis',
labelEventCompleted: 'The Genesis of Bitcoin',
networks: ['mainnet', 'testnet'],
},
'210000': {
labelEvent: 'Bitcoin\'s 1st Halving',
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'420000': {
labelEvent: 'Bitcoin\'s 2nd Halving',
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
networks: ['mainnet', 'testnet'],
},
'630000': {
labelEvent: 'Bitcoin\'s 3rd Halving',
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
networks: ['mainnet', 'testnet'],
},
'709632': { '709632': {
labelEvent: 'Taproot 🌱 activation', labelEvent: 'Taproot 🌱 activation',
labelEventCompleted: 'Taproot 🌱 has been activated!', labelEventCompleted: 'Taproot 🌱 has been activated!',
networks: ['mainnet'],
}, },
'840000': { '840000': {
labelEvent: 'Bitcoin\'s 4th Halving', labelEvent: 'Halving 🥳',
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1050000': {
labelEvent: 'Bitcoin\'s 5th Halving',
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1260000': {
labelEvent: 'Bitcoin\'s 6th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1470000': {
labelEvent: 'Bitcoin\'s 7th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1680000': {
labelEvent: 'Bitcoin\'s 8th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
networks: ['mainnet', 'testnet'],
},
'1890000': {
labelEvent: 'Bitcoin\'s 9th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2100000': {
labelEvent: 'Bitcoin\'s 10th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2310000': {
labelEvent: 'Bitcoin\'s 11th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2520000': {
labelEvent: 'Bitcoin\'s 12th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2730000': {
labelEvent: 'Bitcoin\'s 13th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
networks: ['mainnet', 'testnet'],
},
'2940000': {
labelEvent: 'Bitcoin\'s 14th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
networks: ['mainnet', 'testnet'],
},
'3150000': {
labelEvent: 'Bitcoin\'s 15th Halving',
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
networks: ['mainnet', 'testnet'],
} }
}; };
export const fiatCurrencies = {
AUD: {
name: 'Australian Dollar',
code: 'AUD',
indexed: true,
},
CAD: {
name: 'Canadian Dollar',
code: 'CAD',
indexed: true,
},
CHF: {
name: 'Swiss Franc',
code: 'CHF',
indexed: true,
},
EUR: {
name: 'Euro',
code: 'EUR',
indexed: true,
},
GBP: {
name: 'Pound Sterling',
code: 'GBP',
indexed: true,
},
JPY: {
name: 'Japanese Yen',
code: 'JPY',
indexed: true,
},
USD: {
name: 'US Dollar',
code: 'USD',
indexed: true,
},
};

View File

@@ -7,7 +7,6 @@ import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service'; import { ElectrsApiService } from './services/electrs-api.service';
import { StateService } from './services/state.service'; import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service'; import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service'; import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service'; import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service'; import { AudioService } from './services/audio.service';
@@ -18,7 +17,6 @@ import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service'; import { LanguageService } from './services/language.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy'; import { AppPreloadingStrategy } from './app.preloading-strategy';
@@ -27,7 +25,6 @@ const providers = [
ElectrsApiService, ElectrsApiService,
StateService, StateService,
CacheService, CacheService,
PriceService,
WebsocketService, WebsocketService,
AudioService, AudioService,
SeoService, SeoService,
@@ -37,7 +34,6 @@ const providers = [
LanguageService, LanguageService,
ShortenStringPipe, ShortenStringPipe,
FiatShortenerPipe, FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe, CapAddressPipe,
AppPreloadingStrategy, AppPreloadingStrategy,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true } { provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }

View File

@@ -24,7 +24,7 @@
<td> <td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i> <i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -17,7 +17,7 @@
<tbody *ngIf="blocks.value; else loadingTmpl"> <tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn"> <tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> <td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td> <td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td> <td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td> <td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr> </tr>
@@ -27,9 +27,6 @@
<br> <br>
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination> <ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<div class="clearfix"></div>
<br>
</ng-container> </ng-container>
</div> </div>

View File

@@ -35,7 +35,7 @@
<td> <td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }} &lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline"> <div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i> <i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
</div> </div>
</td> </td>
</tr> </tr>

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