Compare commits

..

1 Commits

Author SHA1 Message Date
softsimon
4a7533013b Automated database creation and migration
fixes #1002
2021-12-17 14:36:34 +09:00
438 changed files with 86518 additions and 107048 deletions

37
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,37 @@
<!--
SUPPORT REQUESTS: This is for reporting bugs in Mempool.
If you have a support request, please join our Keybase group:
https://keybase.io/team/mempool
-->
### Description
<!-- brief description of the bug -->
#### Version
<!-- commit id or version number -->
### Steps to reproduce
<!--if you can reliably reproduce the bug, list the steps here -->
### Expected behaviour
<!--description of the expected behavior -->
### Actual behaviour
<!-- explain what happened instead of the expected behaviour -->
### Screenshots
<!--Screenshots if gui related, drag and drop to add to the issue -->
#### Device or machine
<!-- device/machine used, operating system -->
#### Additional info
<!-- Additional information useful for debugging (e.g. logs) -->

View File

@@ -1,43 +0,0 @@
---
name: 🐛 Bug Report
about: Report bugs (no support requests, please)
---
<!--
SUPPORT REQUESTS:
This is for reporting bugs in Mempool, not for support requests.
If you have a support request, please reach out on Matrix:
https://matrix.to/#/#mempool.support:bitcoin.kyoto
-->
### Description
<!-- brief description of the bug -->
#### Version
<!-- commit id or version number -->
### Steps to reproduce
<!-- if you can reliably reproduce the bug, list the steps here -->
### Expected behaviour
<!-- description of the expected behavior -->
### Actual behaviour
<!-- explain what happened instead of the expected behaviour -->
### Screenshots
<!-- Screenshots if gui related, drag and drop to add to the issue -->
#### Device or machine
<!-- device/machine used, operating system -->
#### Additional info
<!-- Additional information useful for debugging (e.g. logs) -->

View File

@@ -1,27 +0,0 @@
---
name: ✨ Feature Request
about: Request a feature or suggest other enhancements
---
<!--
SUPPORT REQUESTS:
This is for requesting features in Mempool, not for support requests.
If you have a support request, please reach out on Matrix:
https://matrix.to/#/#mempool.support:bitcoin.kyoto
-->
### Description
<!-- brief description of the feature request -->
### Problem to be solved
<!-- description of the the problem you're having -->
### Proposed solution
<!-- explain how you think we should solve the problem -->
#### Additional info
<!-- Additional information useful for implementing (e.g. docs, links, etc.) -->

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 🙋 Need help? Chat with us on Matrix
url: https://matrix.to/#/#mempool.support:bitcoin.kyoto
about: For support requests or general questions
- name: 🌐 Want to help with translations? Use Transifex
url: https://www.transifex.com/mempool/mempool
about: All translations work is done on Transifex

View File

@@ -1,20 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/backend"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: npm
directory: "/frontend"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: docker
directory: "/docker/backend"
schedule:
interval: weekly
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,6 +0,0 @@
<!--
Please do not open pull requests for translations.
All translations work is done on Transifex:
https://www.transifex.com/mempool/mempool
-->

View File

@@ -1,6 +1,7 @@
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress:
runs-on: ${{ matrix.os }}
@@ -17,75 +18,63 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.15.0
node-version: 16.10.0
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Mempool)
uses: cypress-io/github-action@v4
uses: cypress-io/github-action@v2
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
start: npm run start:local-prod
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
group: Tests on ${{ matrix.browser }} (Mempool)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- name: ${{ matrix.browser }} browser tests (Liquid)
uses: cypress-io/github-action@v4
uses: cypress-io/github-action@v2
if: always()
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:liquid
start: npm run start:local-staging
start: npm run start:local-prod
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
spec: cypress/integration/liquid/liquid.spec.ts
group: Tests on ${{ matrix.browser }} (Liquid)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- name: ${{ matrix.browser }} browser tests (Bisq)
uses: cypress-io/github-action@v4
uses: cypress-io/github-action@v2
if: always()
with:
tag: ${{ github.event_name }}
working-directory: frontend
build: npm run config:defaults:bisq
start: npm run start:local-staging
start: npm run start:local-prod
wait-on: 'http://localhost:4200'
wait-on-timeout: 120
record: true
parallel: true
spec: cypress/e2e/bisq/bisq.spec.ts
spec: cypress/integration/bisq/bisq.spec.ts
group: Tests on ${{ matrix.browser }} (Bisq)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}

View File

@@ -11,9 +11,6 @@ on:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
permissions:
contents: read
jobs:
build:
strategy:
@@ -38,24 +35,24 @@ jobs:
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
uses: actions/checkout@v2
- name: Init repo for Dockerization
run: docker/init.sh "$TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480 # v1
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 # v1
uses: docker/setup-buildx-action@v1
id: buildx
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@661fd3eb7f2f20d8c7c84bc2b0509efd7a826628 # v2
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache

2
.nvmrc
View File

@@ -1 +1 @@
v16.15.0
v16.10.0

View File

@@ -1,49 +0,0 @@
# Contributing to The Mempool Open Source Project
Thank you for contributing to The Mempool Open Source Project managed by Mempool Space K.K. (“Mempool”).
In order to clarify the intellectual property license granted with Contributions from any person or entity, Mempool must have a statement on file from each Contributor indicating their agreement to the Contributor License Agreement (“Agreement”). This license is for your protection as a Contributor as well as the protection of Mempool and its other contributors and users; it does not change your rights to use your own Contributions for any other purpose.
When submitting a pull request for the first time, please create a file with a name like `/contributors/{github_username}.txt`, and in the content of that file indicate your agreement to the Contributor License Agreement terms below. An example of what that file should contain can be seen in wiz's agreement file. (This method of CLA "signing" is borrowed from Medium's open source project.)
# Contributor License Agreement
Last Updated: January 25, 2022
By accepting this Agreement, You agree to the following terms and conditions for Your present and future Contributions submitted to Mempool. Except for the license granted herein to Mempool and recipients of software distributed by Mempool, You reserve all right, title, and interest in and to Your Contributions.
### 1. Definitions
“You” (or “Your”) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Mempool. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
“Contribution” shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Mempool for inclusion in, or documentation of, any of the products owned or managed by Mempool (“Work”). For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to Mempool or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Mempool for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.”
### 2. Grant of Copyright License
Subject to the terms and conditions of this Agreement, You hereby grant to Mempool and to recipients of software distributed by Mempool a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
### 3. Grant of Patent License
Subject to the terms and conditions of this Agreement, You hereby grant to Mempool and to recipients of software distributed by Mempool a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
### 4. Authority
You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Mempool, or that your employer has executed a separate Corporate Contributor License Agreement with Mempool.
### 5. Originality
You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware, and which are associated with any part of Your Contributions.
### 6. Support
You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
### 7. Third Party Contributions
Should You wish to submit work that is not Your original creation, You may submit it to Mempool separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Submitted on behalf of a third-party: [named here]”.
### 8. Notifications
You agree to notify Mempool of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
EOF

View File

@@ -25,7 +25,8 @@ help:
.PHONY: init
init:
@echo ''
mkdir -p $(DATA) $(DATA)/mysql $(DATA)/mysql/data
mkdir -p $(DATA) $(DATA)/mysql $(DATA)/mysql/db-scripts $(DATA)/mysql/data
install -v mariadb-structure.sql $(DATA)/mysql/db-scripts
#REF: https://github.com/mempool/mempool/blob/master/docker/README.md
cat docker/docker-compose.yml > docker-compose.yml
cat backend/mempool-config.sample.json > backend/mempool-config.json

View File

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

197
README.md
View File

@@ -1,33 +1,188 @@
# 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 is the fully-featured mempool visualizer, explorer, and API service running at [mempool.space](https://mempool.space/).
Mempool is the fully featured visualizer, explorer, and API service running on [mempool.space](https://mempool.space/), an open source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market to help our transition into a multi-layer ecosystem.
It is an open-source project developed and operated for the benefit of the Bitcoin community, with a focus on the emerging transaction fee market that is evolving Bitcoin into a multi-layer ecosystem.
![mempool](https://mempool.space/resources/screenshots/v2.3.0-dashboard.png)
![mempool](https://mempool.space/resources/screenshots/v2.4.0-dashboard.png)
## Installation Methods
# Installation Methods
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi distro, all the way to an advanced high availability cluster of powerful servers for a production instance. We support the following installation methods, ranked in order from simple to advanced:
Mempool can be self-hosted on a wide variety of your own hardware, ranging from a simple one-click installation on a Raspberry Pi full-node distro all the way to a robust production instance on a powerful FreeBSD server.
1) One-click installation on: [Umbrel](https://github.com/getumbrel/umbrel), [RaspiBlitz](https://github.com/rootzoll/raspiblitz), [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo), or [MyNode](https://github.com/mynodebtc/mynode).
2) [Docker installation on Linux using docker-compose](https://github.com/mempool/mempool/tree/master/docker)
3) [Manual installation on Linux or FreeBSD](https://github.com/mempool/mempool#manual-installation)
4) [Production installation on a powerful FreeBSD server](https://github.com/mempool/mempool/tree/master/production)
5) [High Availability cluster using powerful FreeBSD servers](https://github.com/mempool/mempool/tree/master/production#high-availability)
**Most people should use a one-click install method.** Other install methods are meant for developers and others with experience managing servers.
# Manual Installation
<a id="one-click-installation"></a>
## One-Click Installation
The following instructions are for a manual installation on Linux or FreeBSD. The file and directory paths may need to be changed to match your OS.
Mempool can be conveniently installed on the following full-node distros:
- [Umbrel](https://github.com/getumbrel/umbrel)
- [RaspiBlitz](https://github.com/rootzoll/raspiblitz)
- [RoninDojo](https://code.samourai.io/ronindojo/RoninDojo)
- [myNode](https://github.com/mynodebtc/mynode)
- [Start9](https://github.com/Start9Labs/embassy-os)
## Dependencies
**We highly recommend you deploy your own Mempool instance this way.** No matter which option you pick, you'll be able to get your own fully-sovereign instance of Mempool up quickly without needing to fiddle with any settings.
* [Bitcoin](https://github.com/bitcoin/bitcoin)
* [Electrum](https://github.com/romanz/electrs)
* [NodeJS](https://github.com/nodejs/node)
* [MariaDB](https://github.com/mariadb/server)
* [Nginx](https://github.com/nginx/nginx)
## Advanced Installation Methods
## Mempool
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
Clone the mempool repo, and checkout the latest release tag:
```bash
git clone https://github.com/mempool/mempool
cd mempool
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers and small-scale deployments.
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.
## Bitcoin Core (bitcoind)
Enable RPC and txindex in `bitcoin.conf`:
```bash
rpcuser=mempool
rpcpassword=mempool
txindex=1
```
## MySQL
Install MariaDB from OS package manager:
```bash
# Linux
apt-get install mariadb-server mariadb-client
# macOS
brew install mariadb
mysql.server start
```
Create database and grant privileges:
```bash
MariaDB [(none)]> drop database mempool;
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> create database mempool;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool'@'%' identified by 'mempool';
Query OK, 0 rows affected (0.00 sec)
```
From the mempool repo's top-level folder, import the database structure:
```bash
mysql -u mempool -p mempool < mariadb-structure.sql
```
## Mempool Backend
Install mempool dependencies from npm and build the backend:
```bash
# backend
cd backend
npm install --prod
npm run build
```
In the `backend` folder, make a copy of the sample config and modify it to fit your settings.
```bash
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` to add your Bitcoin Core node RPC credentials:
```bash
{
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": 8999
},
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"USERNAME": "mempool",
"PASSWORD": "mempool",
"DATABASE": "mempool"
}
}
```
Start the backend:
```bash
npm run start
```
When it's running you should see output like this:
```bash
Mempool updated in 0.189 seconds
Updating mempool
Mempool updated in 0.096 seconds
Updating mempool
Mempool updated in 0.099 seconds
Updating mempool
Calculated fee for transaction 1 / 10
Calculated fee for transaction 2 / 10
Calculated fee for transaction 3 / 10
Calculated fee for transaction 4 / 10
Calculated fee for transaction 5 / 10
Calculated fee for transaction 6 / 10
Calculated fee for transaction 7 / 10
Calculated fee for transaction 8 / 10
Calculated fee for transaction 9 / 10
Calculated fee for transaction 10 / 10
Mempool updated in 0.243 seconds
Updating mempool
```
## Mempool Frontend
Install mempool dependencies from npm and build the frontend static HTML/CSS/JS:
```bash
# frontend
cd frontend
npm install --prod
npm run build
```
Install the output into nginx webroot folder:
```bash
sudo rsync -av --delete dist/mempool/ /var/www/
```
## nginx + certbot
Install the supplied nginx.conf and nginx-mempool.conf in /etc/nginx
```bash
# install nginx and certbot
apt-get install -y nginx python3-certbot-nginx
# install the mempool configuration for nginx
cp nginx.conf nginx-mempool.conf /etc/nginx/
# replace example.com with your domain name
certbot --nginx -d example.com
```
If everything went okay you should see the beautiful mempool :grin:
If you get stuck on "loading blocks", this means the websocket can't connect.
Check your nginx proxy setup, firewalls, etc. and open an issue if you need help.

7
backend/.gitignore vendored
View File

@@ -1,10 +1,7 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# production config and external assets
*.json
!mempool-config.sample.json
icons.json
# production config
mempool-config.json
# compiled output
/dist

View File

@@ -1,161 +0,0 @@
# Mempool Backend
These instructions are mostly intended for developers, but can be used as a basis for personal or small-scale production setups.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
See other ways to set up Mempool on [the main README](/../../#installation-methods).
Jump to a section in this doc:
- [Set Up the Backend](#setup)
- [Development Tips](#development-tips)
## Setup
### 1. Clone Mempool Repository
Get the latest Mempool code:
```
git clone https://github.com/mempool/mempool
cd mempool
```
Check out the latest release:
```
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
### 2. Configure Bitcoin Core
Turn on `txindex`, enable RPC, and set RPC credentials in `bitcoin.conf`:
```
txindex=1
server=1
rpcuser=mempool
rpcpassword=mempool
```
### 3. Configure Electrum Server
[Pick an Electrum Server implementation](https://mempool.space/docs/faq#address-lookup-issues), configure it, and make sure it's synced.
**This step is optional.** You can run Mempool without configuring an Electrum Server for it, but address lookups will be disabled.
### 4. Configure MariaDB
_Mempool needs MariaDB v10.5 or later. If you already have MySQL installed, make sure to migrate any existing databases **before** installing MariaDB._
Get MariaDB from your operating system's package manager:
```
# Debian, Ubuntu, etc.
apt-get install mariadb-server mariadb-client
# macOS
brew install mariadb
mysql.server start
```
Create a database and grant privileges:
```
MariaDB [(none)]> drop database mempool;
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> create database mempool;
Query OK, 1 row affected (0.00 sec)
MariaDB [(none)]> grant all privileges on mempool.* to 'mempool'@'%' identified by 'mempool';
Query OK, 0 rows affected (0.00 sec)
```
### 5. Prepare Mempool Backend
#### Build
_Make sure to use Node.js 16.15 and npm 7._
Install dependencies with `npm` and build the backend:
```
cd backend
npm install # add --prod for production
npm run build
```
#### Configure
In the backend folder, make a copy of the sample config file:
```
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` as needed.
In particular, make sure:
- the correct Bitcoin Core RPC credentials are specified in `CORE_RPC`
- the correct `BACKEND` is specified in `MEMPOOL`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "none" if you're not using any Electrum Server
### 6. Run Mempool Backend
Run the Mempool backend:
```
npm run start
```
When it's running, you should see output like this:
```
Mempool updated in 0.189 seconds
Updating mempool
Mempool updated in 0.096 seconds
Updating mempool
Mempool updated in 0.099 seconds
Updating mempool
Calculated fee for transaction 1 / 10
Calculated fee for transaction 2 / 10
Calculated fee for transaction 3 / 10
Calculated fee for transaction 4 / 10
Calculated fee for transaction 5 / 10
Calculated fee for transaction 6 / 10
Calculated fee for transaction 7 / 10
Calculated fee for transaction 8 / 10
Calculated fee for transaction 9 / 10
Calculated fee for transaction 10 / 10
Mempool updated in 0.243 seconds
Updating mempool
```
### 7. Set Up Mempool Frontend
With the backend configured and running, proceed to set up the [Mempool frontend](../frontend#manual-setup).
## Development Tips
### Set Up Backend Watchers
The Mempool backend is static. TypeScript scripts are compiled into the `dist` folder and served through a Node.js web server.
As a result, for development purposes, you may find it helpful to set up backend watchers to avoid the manual shutdown/recompile/restart command-line cycle.
First, install `nodemon` and `ts-node`:
```
npm install -g ts-node nodemon
```
Then, run the watcher:
```
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`.

View File

@@ -12,14 +12,8 @@
"BLOCK_WEIGHT_UNITS": 4000000,
"INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"INDEXING_BLOCKS_AMOUNT": 11000,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1,
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug"
"PRICE_FEED_UPDATE_INTERVAL": 3600,
"USE_SECOND_NODE_FOR_MINFEE": false
},
"CORE_RPC": {
"HOST": "127.0.0.1",
@@ -45,7 +39,6 @@
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"SOCKET": "/var/run/mysql/mysql.sock",
"DATABASE": "mempool",
"USERNAME": "mempool",
"PASSWORD": "mempool"
@@ -64,25 +57,5 @@
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"SOCKS5PROXY": {
"ENABLED": false,
"USE_ONION": true,
"HOST": "127.0.0.1",
"PORT": 9050,
"USERNAME": "",
"PASSWORD": ""
},
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
"LIQUID_API": "https://liquid.network/api/v1",
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
"BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
}
}

1085
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "mempool-backend",
"version": "2.4.0-dev",
"version": "2.3.0-dev",
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space",
@@ -28,21 +28,23 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@mempool/bitcoin": "^3.0.3",
"@mempool/electrum-client": "^1.1.7",
"axios": "~0.27.2",
"@types/ws": "8.2.2",
"axios": "0.24.0",
"bitcoinjs-lib": "6.0.1",
"crypto-js": "^4.0.0",
"express": "^4.18.0",
"express": "^4.17.1",
"locutus": "^2.0.12",
"mysql2": "2.3.3",
"node-worker-threads-pool": "^1.5.1",
"socks-proxy-agent": "^6.2.0",
"typescript": "~4.7.2",
"ws": "~8.7.0"
"node-worker-threads-pool": "^1.4.3",
"typescript": "4.4.4",
"ws": "8.3.0"
},
"devDependencies": {
"@types/compression": "^1.7.2",
"@types/ws": "~8.5.3",
"@types/express": "^4.17.13",
"@types/compression": "^1.0.1",
"@types/express": "^4.17.2",
"@types/locutus": "^0.0.6",
"tslint": "^6.1.0"
}
}

View File

@@ -2,7 +2,6 @@ import * as fs from 'fs';
import * as os from 'os';
import logger from '../logger';
import { IBackendInfo } from '../mempool.interfaces';
const { spawnSync } = require('child_process');
class BackendInfo {
private gitCommitHash = '';
@@ -28,23 +27,10 @@ class BackendInfo {
}
private setLatestCommitHash(): void {
//TODO: share this logic with `generate-config.js`
if (process.env.DOCKER_COMMIT_HASH) {
this.gitCommitHash = process.env.DOCKER_COMMIT_HASH;
} else {
try {
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
if (!gitRevParse.error) {
const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
this.gitCommitHash = output ? output : '?';
} else if (gitRevParse.error.code === 'ENOENT') {
console.log('git not found, cannot parse git hash');
this.gitCommitHash = '?';
}
} catch (e: any) {
console.log('Could not load git commit info: ' + e.message);
this.gitCommitHash = '?';
}
try {
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
} catch (e) {
logger.err('Could not load git commit info: ' + (e instanceof Error ? e.message : e));
}
}

View File

@@ -1,14 +1,10 @@
import config from '../../config';
import * as fs from 'fs';
import axios, { AxiosResponse } from 'axios';
import * as http from 'http';
import * as https from 'https';
import { SocksProxyAgent } from 'socks-proxy-agent';
import axios from 'axios';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import { StaticPool } from 'node-worker-threads-pool';
import backendInfo from '../backend-info';
import logger from '../../logger';
class Bisq {
@@ -39,13 +35,7 @@ class Bisq {
constructor() {}
startBisqService(): void {
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.checkForBisqDataFolder();
this.loadBisqDumpFile();
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
@@ -100,7 +90,7 @@ class Bisq {
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
return process.exit(1);
}
}
@@ -147,59 +137,12 @@ class Bisq {
}, 2000);
});
}
private async updatePrice() {
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 BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
const isHTTP = (new URL(BISQ_URL).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) {
const 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);
}
}
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
}
private updatePrice() {
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
.then((response) => {
const prices: number[] = [];
data.data.forEach((trade) => {
response.data.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
@@ -207,24 +150,19 @@ class Bisq {
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
logger.debug('Successfully updated Bisq market price');
break;
} catch (e) {
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
retry++;
}
}
}).catch((err) => {
logger.err('Error updating Bisq market price: ' + err);
});
}
private async loadBisqDumpFile(): Promise<void> {
this.allBlocks = [];
try {
await this.loadData();
const data = await this.loadData();
await this.loadBisqBlocksDump(data);
this.buildIndex();
this.calculateStats();
} catch (e) {
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
}
}
@@ -303,61 +241,36 @@ class Bisq {
};
}
private async loadData(): Promise<any> {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
throw new Error(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
}
const readline = require('readline');
const events = require('events');
const rl = readline.createInterface({
input: fs.createReadStream(Bisq.BLOCKS_JSON_FILE_PATH),
crlfDelay: Infinity
});
let blockBuffer = '';
let readingBlock = false;
let lineCount = 1;
private async loadBisqBlocksDump(cacheData: string): Promise<void> {
const start = new Date().getTime();
logger.debug('Processing Bisq data dump...');
rl.on('line', (line) => {
if (lineCount === 2) {
line = line.replace(' "chainHeight": ', '');
this.latestBlockHeight = parseInt(line, 10);
if (cacheData && cacheData.length !== 0) {
logger.debug('Processing Bisq data dump...');
const data: BisqBlocks = await this.jsonParsePool.exec(cacheData);
if (data.blocks && data.blocks.length !== this.allBlocks.length) {
this.allBlocks = data.blocks;
this.allBlocks.reverse();
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
logger.debug('Bisq dump processed in ' + time + ' ms (worker thread)');
} else {
throw new Error(`Bisq dump didn't contain any blocks`);
}
}
}
if (line === ' {') {
readingBlock = true;
} else if (line === ' },') {
blockBuffer += '}';
try {
const block: BisqBlock = JSON.parse(blockBuffer);
this.allBlocks.push(block);
readingBlock = false;
blockBuffer = '';
} catch (e) {
logger.debug(blockBuffer);
throw Error(`Unable to parse Bisq data dump at line ${lineCount}` + (e instanceof Error ? e.message : e));
private loadData(): Promise<string> {
return new Promise((resolve, reject) => {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
return reject(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist`);
}
fs.readFile(Bisq.BLOCKS_JSON_FILE_PATH, 'utf8', (err, data) => {
if (err) {
reject(err);
}
}
if (readingBlock === true) {
blockBuffer += line;
}
++lineCount;
resolve(data);
});
});
await events.once(rl, 'close');
this.allBlocks.reverse();
this.blocks = this.allBlocks.filter((block) => block.txs.length > 0);
const time = new Date().getTime() - start;
logger.debug('Bisq dump processed in ' + time + ' ms');
}
}

View File

@@ -1,7 +1,7 @@
import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
const strtotime = require('./strtotime');
import * as datetime from 'locutus/php/datetime';
class BisqMarketsApi {
private cryptoCurrencyData: Currency[] = [];
@@ -312,7 +312,7 @@ class BisqMarketsApi {
getTickerFromMarket(market: string): Ticker | null {
let ticker: Ticker;
const timestamp_from = strtotime('-24 hour');
const timestamp_from = datetime.strtotime('-24 hour');
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
@@ -638,13 +638,13 @@ class BisqMarketsApi {
case 'half_day':
return (ts - (ts % (3600 * 12)));
case 'day':
return strtotime('midnight today', ts);
return datetime.strtotime('midnight today', ts);
case 'week':
return strtotime('midnight sunday last week', ts);
return datetime.strtotime('midnight sunday last week', ts);
case 'month':
return strtotime('midnight first day of this month', ts);
return datetime.strtotime('midnight first day of this month', ts);
case 'year':
return strtotime('midnight first day of january', ts);
return datetime.strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval: ' + interval);
}

View File

@@ -26,13 +26,7 @@ class Bisq {
constructor() {}
startBisqService(): void {
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service (markets) in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.checkForBisqDataFolder();
this.loadBisqDumpFile();
this.startBisqDirectoryWatcher();
}
@@ -40,7 +34,7 @@ class Bisq {
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
throw new Error(`Cannot load BISQ ${Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency} file`);
return process.exit(1);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;
@@ -12,7 +12,6 @@ export interface AbstractBitcoinApi {
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[];
$sendRawTransaction(rawTransaction: string): Promise<string>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
}
export interface BitcoinRpcCredentials {
host: string;

View File

@@ -4,7 +4,6 @@ export namespace IBitcoinApi {
size: number; // (numeric) Current tx count
bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
usage: number; // (numeric) Total memory usage for the mempool
total_fee: number; // (numeric) Total fees of transactions in the mempool
maxmempool: number; // (numeric) Maximum memory usage for the mempool
mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions

View File

@@ -14,54 +14,33 @@ class BitcoinApi implements AbstractBitcoinApi {
this.bitcoindClient = bitcoinClient;
}
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
// Special case to fetch the Coinbase transaction
if (txId === '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b') {
return this.$returnCoinbaseTransaction();
}
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
transaction.vout.forEach((vout) => {
vout.value = Math.round(vout.value * 100000000);
vout.value = vout.value * 100000000;
});
return transaction;
}
return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
})
.catch((e: Error) => {
if (e.message.startsWith('The genesis block coinbase')) {
return this.$returnCoinbaseTransaction();
}
throw e;
return this.$convertTransaction(transaction, addPrevout);
});
}
$getBlockHeightTip(): Promise<number> {
return this.bitcoindClient.getChainTips()
.then((result: IBitcoinApi.ChainTips[]) => {
return result.find(tip => tip.status === 'active')!.height;
});
.then((result: IBitcoinApi.ChainTips[]) => result[0].height);
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
@@ -88,7 +67,7 @@ class BitcoinApi implements AbstractBitcoinApi {
}
return this.bitcoindClient.getBlock(hash)
.then((block: IBitcoinApi.Block) => BitcoinApi.convertBlock(block));
.then((block: IBitcoinApi.Block) => this.convertBlock(block));
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
@@ -104,49 +83,26 @@ class BitcoinApi implements AbstractBitcoinApi {
}
$getAddressPrefix(prefix: string): string[] {
const found: { [address: string]: string } = {};
const found: string[] = [];
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
found[vout.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
found.push(vout.scriptpubkey_address);
if (found.length >= 10) {
return found;
}
}
}
}
return Object.keys(found);
return found;
}
$sendRawTransaction(rawTransaction: string): Promise<string> {
return this.bitcoindClient.sendRawTransaction(rawTransaction);
}
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
const outSpends: IEsploraApi.Outspend[] = [];
const tx = await this.$getRawTransaction(txId, true, false);
for (let i = 0; i < tx.vout.length; i++) {
if (tx.status && tx.status.block_height === 0) {
outSpends.push({
spent: false
});
} else {
const txOut = await this.bitcoindClient.getTxOut(txId, i);
outSpends.push({
spent: txOut === null,
});
}
}
return outSpends;
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
}
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
let esploraTransaction: IEsploraApi.Transaction = {
txid: transaction.txid,
version: transaction.version,
@@ -161,11 +117,11 @@ class BitcoinApi implements AbstractBitcoinApi {
esploraTransaction.vout = transaction.vout.map((vout) => {
return {
value: Math.round(vout.value * 100000000),
value: vout.value * 100000000,
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
});
@@ -175,7 +131,7 @@ class BitcoinApi implements AbstractBitcoinApi {
is_coinbase: !!vin.coinbase,
prevout: null,
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.asm) || '',
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,
@@ -192,15 +148,35 @@ class BitcoinApi implements AbstractBitcoinApi {
};
}
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
} else if (!transaction.confirmations) {
if (transaction.confirmations) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
} else {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
}
}
return esploraTransaction;
}
private convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
private translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
@@ -210,14 +186,13 @@ class BitcoinApi implements AbstractBitcoinApi {
'witness_v0_scripthash': 'v0_p2wsh',
'witness_v1_taproot': 'v1_p2tr',
'nonstandard': 'nonstandard',
'multisig': 'multisig',
'nulldata': 'op_return'
};
if (map[outputType]) {
return map[outputType];
} else {
return 'unknown';
return '';
}
}
@@ -234,7 +209,7 @@ class BitcoinApi implements AbstractBitcoinApi {
} else {
mempoolEntry = await this.$getMempoolEntry(transaction.txid);
}
transaction.fee = Math.round(mempoolEntry.fees.base * 100000000);
transaction.fee = mempoolEntry.fees.base * 100000000;
return transaction;
}
@@ -243,7 +218,7 @@ class BitcoinApi implements AbstractBitcoinApi {
if (vin.prevout) {
continue;
}
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
const innerTx = await this.$getRawTransaction(vin.txid, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
@@ -251,14 +226,12 @@ class BitcoinApi implements AbstractBitcoinApi {
}
protected $returnCoinbaseTransaction(): Promise<IEsploraApi.Transaction> {
return this.bitcoindClient.getBlockHash(0).then((hash: string) =>
this.bitcoindClient.getBlock(hash, 2)
.then((block: IBitcoinApi.Block) => {
return this.$convertTransaction(Object.assign(block.tx[0], {
confirmations: blocks.getCurrentBlockHeight() + 1,
blocktime: block.time }), false);
})
);
return this.bitcoindClient.getBlock('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', 2)
.then((block: IBitcoinApi.Block) => {
return this.$convertTransaction(Object.assign(block.tx[0], {
confirmations: blocks.getCurrentBlockHeight() + 1,
blocktime: 1231006505 }), false);
});
}
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
@@ -269,95 +242,42 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getRawMemPool(true);
}
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
if (transaction.vin[0].is_coinbase) {
transaction.fee = 0;
return transaction;
}
let totalIn = 0;
for (let i = 0; i < transaction.vin.length; i++) {
if (lazyPrevouts && i > 12) {
transaction.vin[i].lazy = true;
continue;
for (const vin of transaction.vin) {
const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
if (addPrevout) {
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
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) {
transaction.fee = -1;
} else {
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
totalIn += innerTx.vout[vin.vout].value;
}
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
return transaction;
}
private convertScriptSigAsm(hex: string): string {
const buf = Buffer.from(hex, 'hex');
private convertScriptSigAsm(str: string): string {
const a = str.split(' ');
const b: string[] = [];
let i = 0;
while (i < buf.length) {
const op = buf[i];
if (op >= 0x01 && op <= 0x4e) {
i++;
let push: number;
if (op === 0x4c) {
push = buf.readUInt8(i);
b.push('OP_PUSHDATA1');
i += 1;
} else if (op === 0x4d) {
push = buf.readUInt16LE(i);
b.push('OP_PUSHDATA2');
i += 2;
} else if (op === 0x4e) {
push = buf.readUInt32LE(i);
b.push('OP_PUSHDATA4');
i += 4;
} else {
push = op;
b.push('OP_PUSHBYTES_' + push);
}
const data = buf.slice(i, i + push);
if (data.length !== push) {
break;
}
b.push(data.toString('hex'));
i += data.length;
a.forEach((chunk) => {
if (chunk.substr(0, 3) === 'OP_') {
chunk = chunk.replace(/^OP_(\d+)/, 'OP_PUSHNUM_$1');
chunk = chunk.replace('OP_CHECKSEQUENCEVERIFY', 'OP_CSV');
b.push(chunk);
} else {
if (op === 0x00) {
chunk = chunk.replace('[ALL]', '01');
if (chunk === '0') {
b.push('OP_0');
} else if (op === 0x4f) {
b.push('OP_PUSHNUM_NEG1');
} else if (op === 0xb1) {
b.push('OP_CLTV');
} else if (op === 0xb2) {
b.push('OP_CSV');
} else if (op === 0xba) {
b.push('OP_CHECKSIGADD');
} else {
const opcode = bitcoinjs.script.toASM([ op ]);
if (opcode && op < 0xfd) {
if (/^OP_(\d+)$/.test(opcode)) {
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
} else {
b.push(opcode);
}
} else {
b.push('OP_RETURN_' + op);
}
b.push('OP_PUSHBYTES_' + Math.round(chunk.length / 2) + ' ' + chunk);
}
i += 1;
}
}
});
return b.join(' ');
}
@@ -368,21 +288,21 @@ class BitcoinApi implements AbstractBitcoinApi {
if (vin.prevout.scriptpubkey_type === 'p2sh') {
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
vin.inner_redeemscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(redeemScript, 'hex')));
if (vin.witness && vin.witness.length > 2) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
}
}
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
const witnessScript = vin.witness[vin.witness.length - 1];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
}
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
const witnessScript = vin.witness[vin.witness.length - 2];
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
vin.inner_witnessscript_asm = this.convertScriptSigAsm(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
}
}

View File

@@ -1,5 +1,5 @@
import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import * as bitcoin from '@mempool/bitcoin';
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
const nodeRpcCredentials: BitcoinRpcCredentials = {

View File

@@ -1,5 +1,5 @@
import config from '../../config';
const bitcoin = require('../../rpc-api/index');
import * as bitcoin from '@mempool/bitcoin';
import { BitcoinRpcCredentials } from './bitcoin-api-abstract-factory';
const nodeRpcCredentials: BitcoinRpcCredentials = {

View File

@@ -33,8 +33,6 @@ export namespace IEsploraApi {
// Elements
is_pegin?: boolean;
issuance?: Issuance;
// Custom
lazy?: boolean;
}
interface Issuance {
@@ -115,9 +113,9 @@ export namespace IEsploraApi {
export interface Outspend {
spent: boolean;
txid?: string;
vin?: number;
status?: Status;
txid: string;
vin: number;
status: Status;
}
export interface Asset {

View File

@@ -60,10 +60,6 @@ class ElectrsApi implements AbstractBitcoinApi {
$sendRawTransaction(rawTransaction: string): Promise<string> {
throw new Error('Method not implemented.');
}
$getOutspends(): Promise<IEsploraApi.Outspend[]> {
throw new Error('Method not implemented.');
}
}
export default ElectrsApi;

View File

@@ -2,23 +2,11 @@ import config from '../config';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, PoolTag, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { BlockExtended, TransactionExtended } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
import bitcoinClient from './bitcoin/bitcoin-client';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import poolsRepository from '../repositories/PoolsRepository';
import blocksRepository from '../repositories/BlocksRepository';
import loadingIndicators from './loading-indicators';
import BitcoinApi from './bitcoin/bitcoin-api';
import { prepareBlock } from '../utils/blocks-utils';
import BlocksRepository from '../repositories/BlocksRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import RatesRepository from '../repositories/RatesRepository';
import poolsParser from './pools-parser';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -42,264 +30,11 @@ class Blocks {
this.newBlockCallbacks.push(fn);
}
/**
* Return the list of transaction for a block
* @param blockHash
* @param blockHeight
* @param onlyCoinbase - Set to true if you only need the coinbase transaction
* @returns Promise<TransactionExtended[]>
*/
private async $getTransactionsExtended(
blockHash: string,
blockHeight: number,
onlyCoinbase: boolean,
quiet: boolean = false,
): Promise<TransactionExtended[]> {
const transactions: TransactionExtended[] = [];
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const mempool = memPool.getMempool();
let transactionsFound = 0;
let transactionsFetched = 0;
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
// We update blocks before the mempool (index.ts), therefore we can
// optimize here by directly fetching txs in the "outdated" mempool
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) {
// Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...)
if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam
logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`);
}
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
transactions.push(tx);
transactionsFetched++;
} catch (e) {
if (i === 0) {
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
throw new Error(msg);
} else {
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
}
}
}
if (onlyCoinbase === true) {
break; // Fetch the first transaction and exit
}
}
transactions.forEach((tx) => {
if (!tx.cpfpChecked) {
Common.setRelativesAndGetCpfpInfo(tx, mempool); // Child Pay For Parent
}
});
if (!quiet) {
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`);
}
return transactions;
}
/**
* Return a block with additional data (reward, coinbase, fees...)
* @param block
* @param transactions
* @returns BlockExtended
*/
private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise<BlockExtended> {
const blockExtended: BlockExtended = Object.assign({ extras: {} }, block);
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
if (block.height === 0) {
blockExtended.extras.medianFee = 0; // 50th percentiles
blockExtended.extras.feeRange = [0, 0, 0, 0, 0, 0, 0];
blockExtended.extras.totalFees = 0;
blockExtended.extras.avgFee = 0;
blockExtended.extras.avgFeeRate = 0;
} else {
const stats = await bitcoinClient.getBlockStats(block.id, [
'feerate_percentiles', 'minfeerate', 'maxfeerate', 'totalfee', 'avgfee', 'avgfeerate'
]);
blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles
blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat();
blockExtended.extras.totalFees = stats.totalfee;
blockExtended.extras.avgFee = stats.avgfee;
blockExtended.extras.avgFeeRate = stats.avgfeerate;
}
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
} else {
if (config.DATABASE.ENABLED === true) {
pool = await poolsRepository.$getUnknownPool();
} else {
pool = poolsParser.unknownPool;
}
}
if (!pool) { // We should never have this situation in practise
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
`Check your "pools" table entries`);
return blockExtended;
}
blockExtended.extras.pool = {
id: pool.id,
name: pool.name,
slug: pool.slug,
};
}
return blockExtended;
}
/**
* Try to find which miner found the block
* @param txMinerInfo
* @returns
*/
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise<PoolTag> {
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
if (config.DATABASE.ENABLED === true) {
return await poolsRepository.$getUnknownPool();
} else {
return poolsParser.unknownPool;
}
}
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const address = txMinerInfo.vout[0].scriptpubkey_address;
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
pools = await poolsRepository.$getPools();
} else {
pools = poolsParser.miningPools;
}
for (let i = 0; i < pools.length; ++i) {
if (address !== undefined) {
const addresses: string[] = JSON.parse(pools[i].addresses);
if (addresses.indexOf(address) !== -1) {
return pools[i];
}
}
const regexes: string[] = JSON.parse(pools[i].regexes);
for (let y = 0; y < regexes.length; ++y) {
const regex = new RegExp(regexes[y], 'i');
const match = asciiScriptSig.match(regex);
if (match !== null) {
return pools[i];
}
}
}
if (config.DATABASE.ENABLED === true) {
return await poolsRepository.$getUnknownPool();
} else {
return poolsParser.unknownPool;
}
}
/**
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
public async $generateBlockDatabase() {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
return;
}
try {
let currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`);
loadingIndicators.setProgress('block-indexing', 0);
const chunkSize = 10000;
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0;
let newlyIndexed = 0;
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
while (currentBlockHeight >= lastBlockToIndex) {
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights(
currentBlockHeight, endBlock);
if (missingBlockHeights.length <= 0) {
currentBlockHeight -= chunkSize;
continue;
}
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`);
for (const blockHeight of missingBlockHeights) {
if (blockHeight < lastBlockToIndex) {
break;
}
++indexedThisRun;
++totalIndexed;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
}
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
newlyIndexed++;
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
currentBlockHeight -= chunkSize;
}
logger.info(`Indexed ${newlyIndexed} blocks`);
loadingIndicators.setProgress('block-indexing', 100);
} catch (e) {
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
loadingIndicators.setProgress('block-indexing', 100);
return;
}
const chainValid = await BlocksRepository.$validateChain();
if (!chainValid) {
indexer.reindex();
}
}
public async $updateBlocks() {
let fastForwarded = false;
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
if (this.blocks.length === 0) {
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
} else {
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
}
@@ -307,9 +42,6 @@ class Blocks {
if (blockHeightTip - this.currentBlockHeight > config.MEMPOOL.INITIAL_BLOCKS_AMOUNT * 2) {
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${config.MEMPOOL.INITIAL_BLOCKS_AMOUNT} recent blocks`);
this.currentBlockHeight = blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT;
fastForwarded = true;
logger.info(`Re-indexing skipped blocks and corresponding hashrates data`);
indexer.reindex(); // Make sure to index the skipped blocks #1619
}
if (!this.lastDifficultyAdjustmentTime) {
@@ -317,53 +49,69 @@ class Blocks {
if (blockchainInfo.blocks === blockchainInfo.headers) {
const heightDiff = blockHeightTip % 2016;
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const block = await bitcoinApi.$getBlock(blockHash);
this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty;
if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`);
}
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`);
} else {
logger.debug(`Blockchain headers (${blockchainInfo.headers}) and blocks (${blockchainInfo.blocks}) not in sync. Waiting...`);
}
}
while (this.currentBlockHeight < blockHeightTip) {
if (this.currentBlockHeight < blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT) {
if (this.currentBlockHeight === 0) {
this.currentBlockHeight = blockHeightTip;
} else {
this.currentBlockHeight++;
logger.debug(`New block found (#${this.currentBlockHeight})!`);
}
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, transactions);
const transactions: TransactionExtended[] = [];
if (Common.indexingEnabled()) {
if (!fastForwarded) {
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock['hash']) {
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
await BlocksRepository.$deleteBlocksFrom(lastBlock['height'] - 10);
await HashratesRepository.$deleteLastEntries();
for (let i = 10; i >= 0; --i) {
await this.$indexBlock(lastBlock['height'] - i);
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
const block = await bitcoinApi.$getBlock(blockHash);
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
const mempool = memPool.getMempool();
let transactionsFound = 0;
for (let i = 0; i < txIds.length; i++) {
if (mempool[txIds[i]]) {
transactions.push(mempool[txIds[i]]);
transactionsFound++;
} else if (config.MEMPOOL.BACKEND === 'esplora' || memPool.isInSync() || i === 0) {
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
try {
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
transactions.push(tx);
} catch (e) {
logger.debug('Error fetching block tx: ' + (e instanceof Error ? e.message : e));
if (i === 0) {
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
}
}
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
}
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
}
transactions.forEach((tx) => {
if (!tx.cpfpChecked) {
Common.setRelativesAndGetCpfpInfo(tx, mempool);
}
});
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${txIds.length - transactionsFound} not found.`);
const blockExtended: BlockExtended = Object.assign({}, block);
blockExtended.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
blockExtended.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
transactions.shift();
transactions.sort((a, b) => b.effectiveFeePerVsize - a.effectiveFeePerVsize);
blockExtended.medianFee = transactions.length > 0 ? Common.median(transactions.map((tx) => tx.effectiveFeePerVsize)) : 0;
blockExtended.feeRange = transactions.length > 0 ? Common.getFeesInRange(transactions, 8) : [0, 0];
if (block.height % 2016 === 0) {
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
@@ -379,111 +127,12 @@ class Blocks {
if (this.newBlockCallbacks.length) {
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
}
if (!memPool.hasPriority()) {
if (memPool.isInSync()) {
diskCache.$saveCacheToDisk();
}
}
}
/**
* Index a block if it's missing from the database. Returns the block after indexing
*/
public async $indexBlock(height: number): Promise<BlockExtended> {
const dbBlock = await blocksRepository.$getBlockByHeight(height);
if (dbBlock != null) {
return prepareBlock(dbBlock);
}
const blockHash = await bitcoinApi.$getBlockHash(height);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
return prepareBlock(blockExtended);
}
/**
* 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> {
// Check the memory cache
const blockByHash = this.getBlocks().find((b) => b.id === hash);
if (blockByHash) {
return blockByHash;
}
// Block has already been indexed
if (Common.indexingEnabled()) {
const dbBlock = await blocksRepository.$getBlockByHash(hash);
if (dbBlock != null) {
return prepareBlock(dbBlock);
}
}
const block = await bitcoinApi.$getBlock(hash);
// Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return block;
}
// Bitcoin network, add our custom data on top
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) {
delete(blockExtended['coinbaseTx']);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
return blockExtended;
}
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try {
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
const returnBlocks: BlockExtended[] = [];
if (currentHeight < 0) {
return returnBlocks;
}
if (currentHeight === 0 && Common.indexingEnabled()) {
currentHeight = await blocksRepository.$mostRecentBlockHeight();
}
// 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++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
currentHeight--;
}
return returnBlocks;
} catch (e) {
throw e;
}
}
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}

View File

@@ -1,14 +1,7 @@
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
import config from '../config';
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
: '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
static _isLiquid = config.MEMPOOL.NETWORK === 'liquid' || config.MEMPOOL.NETWORK === 'liquidtestnet';
static isLiquid(): boolean {
return this._isLiquid;
}
static nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
static median(numbers: number[]) {
let medianNr = 0;
@@ -77,7 +70,7 @@ export class Common {
};
}
static sleep$(ms: number): Promise<void> {
static sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
@@ -114,7 +107,7 @@ export class Common {
totalFees += tx.bestDescendant.fee;
}
tx.effectiveFeePerVsize = Math.max(Common.isLiquid() ? 0.1 : 1, totalFees / (totalWeight / 4));
tx.effectiveFeePerVsize = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, totalFees / (totalWeight / 4));
tx.cpfpChecked = true;
return {
@@ -154,27 +147,4 @@ export class Common {
});
return parents;
}
static getSqlInterval(interval: string | null): string | null {
switch (interval) {
case '24h': return '1 DAY';
case '3d': return '3 DAY';
case '1w': return '1 WEEK';
case '1m': return '1 MONTH';
case '3m': return '3 MONTH';
case '6m': return '6 MONTH';
case '1y': return '1 YEAR';
case '2y': return '2 YEAR';
case '3y': return '3 YEAR';
default: return null;
}
}
static indexingEnabled(): boolean {
return (
['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
);
}
}

View File

@@ -1,370 +1,75 @@
import config from '../config';
import DB from '../database';
import { DB } from '../database';
import logger from '../logger';
import { Common } from './common';
class DatabaseMigration {
private static currentVersion = 19;
private static currentVersion = 1;
private queryTimeout = 120000;
private statisticsAddedIndexed = false;
constructor() { }
/**
* Entry point
*/
public async $initializeOrMigrateDatabase(): Promise<void> {
logger.debug('MIGRATIONS: Running migrations');
await this.$printDatabaseVersion();
// First of all, if the `state` database does not exist, create it so we can track migration version
if (!await this.$checkIfTableExists('state')) {
logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
try {
await this.$createMigrationStateTable();
} catch (e) {
logger.err('MIGRATIONS: Unable to create `state` table, aborting in 10 seconds. ' + e);
await Common.sleep$(10000);
process.exit(-1);
}
logger.debug('MIGRATIONS: `state` table initialized.');
if (!await this.$checkIfTableExists('statistics')) {
await this.$initializeDatabaseTables();
}
let databaseSchemaVersion = 0;
try {
databaseSchemaVersion = await this.$getSchemaVersionFromDatabase();
} catch (e) {
logger.err('MIGRATIONS: Unable to get current database migration version, aborting in 10 seconds. ' + e);
await Common.sleep$(10000);
process.exit(-1);
}
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
logger.debug('MIGRATIONS: Nothing to do.');
return;
}
// Now, create missing tables. Those queries cannot be wrapped into a transaction unfortunately
try {
await this.$createMissingTablesAndIndexes(databaseSchemaVersion);
} catch (e) {
logger.err('MIGRATIONS: Unable to create required tables, aborting in 10 seconds. ' + e);
await Common.sleep$(10000);
process.exit(-1);
}
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
logger.notice('MIGRATIONS: Upgrading database schema');
try {
if (await this.$checkIfTableExists('state')) {
const databaseSchemaVersion = await this.$getSchemaVersionFromDatabase();
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
} catch (e) {
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
}
}
return;
}
/**
* Create all missing tables
*/
private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) {
await this.$setStatisticsAddedIndexedFlag(databaseSchemaVersion);
const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
try {
await this.$executeQuery(this.getCreateElementsTableQuery(), await this.$checkIfTableExists('elements_pegs'));
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
if (databaseSchemaVersion < 2 && this.statisticsAddedIndexed === false) {
await this.$executeQuery(`CREATE INDEX added ON statistics (added);`);
}
if (databaseSchemaVersion < 3) {
await this.$executeQuery(this.getCreatePoolsTableQuery(), await this.$checkIfTableExists('pools'));
}
if (databaseSchemaVersion < 4) {
await this.$executeQuery('DROP table IF EXISTS blocks;');
await this.$executeQuery(this.getCreateBlocksTableQuery(), await this.$checkIfTableExists('blocks'));
}
if (databaseSchemaVersion < 5 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 6 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
// Cleanup original blocks fields type
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
// We also fix the pools.id type so we need to drop/re-create the foreign key
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
// Add new block indexing fields
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
}
if (databaseSchemaVersion < 7 && isBitcoin === true) {
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
}
if (databaseSchemaVersion < 8 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
}
if (databaseSchemaVersion < 9 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
}
if (databaseSchemaVersion < 10 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
}
if (databaseSchemaVersion < 11 && isBitcoin === true) {
logger.warn(`'blocks' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE blocks;'); // Need to re-index
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 12 && isBitcoin === true) {
// No need to re-index because the new data type can contain larger values
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 13 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 14 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
}
if (databaseSchemaVersion < 16 && isBitcoin === true) {
logger.warn(`'hashrates' table has been truncated. Re-indexing from scratch.`);
await this.$executeQuery('TRUNCATE hashrates;'); // Need to re-index because we changed timestamps
}
if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
}
if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
}
if (databaseSchemaVersion < 19) {
await this.$executeQuery(this.getCreateRatesTableQuery(), await this.$checkIfTableExists('rates'));
}
} catch (e) {
throw e;
} else {
await this.$migrateTableSchemaFromVersion(0);
}
}
/**
* Special case here for the `statistics` table - It appeared that somehow some dbs already had the `added` field indexed
* while it does not appear in previous schemas. The mariadb command "CREATE INDEX IF NOT EXISTS" is not supported on
* older mariadb version. Therefore we set a flag here in order to know if the index needs to be created or not before
* running the migration process
*/
private async $setStatisticsAddedIndexedFlag(databaseSchemaVersion: number) {
if (databaseSchemaVersion >= 2) {
this.statisticsAddedIndexed = true;
return;
}
try {
// We don't use "CREATE INDEX IF NOT EXISTS" because it is not supported on old mariadb version 5.X
const query = `SELECT COUNT(1) hasIndex FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_schema=DATABASE() AND table_name='statistics' AND index_name='added';`;
const [rows] = await this.$executeQuery(query, true);
if (rows[0].hasIndex === 0) {
logger.debug('MIGRATIONS: `statistics.added` is not indexed');
this.statisticsAddedIndexed = false;
} else if (rows[0].hasIndex === 1) {
logger.debug('MIGRATIONS: `statistics.added` is already indexed');
this.statisticsAddedIndexed = true;
}
} catch (e) {
// Should really never happen but just in case it fails, we just don't execute
// any query related to this indexing so it won't fail if the index actually already exists
logger.err('MIGRATIONS: Unable to check if `statistics.added` INDEX exist or not.');
this.statisticsAddedIndexed = true;
private async $initializeDatabaseTables(): Promise<void> {
const connection = await DB.pool.getConnection();
for (const query of this.getInitializeTableQueries()) {
await connection.query<any>({ sql: query, timeout: this.queryTimeout });
}
connection.release();
logger.info(`Initial database tables have been created`);
}
/**
* Small query execution wrapper to log all executed queries
*/
private async $executeQuery(query: string, silent: boolean = false): Promise<any> {
if (!silent) {
logger.debug('MIGRATIONS: Execute query:\n' + query);
private async $migrateTableSchemaFromVersion(version: number): Promise<void> {
const connection = await DB.pool.getConnection();
for (const query of this.getMigrationQueriesFromVersion(version)) {
await connection.query<any>({ sql: query, timeout: this.queryTimeout });
}
return DB.query({ sql: query, timeout: this.queryTimeout });
connection.release();
await this.$updateToLatestSchemaVersion();
logger.info(`Database schema have been migrated from version ${version} to ${DatabaseMigration.currentVersion} (latest version)`);
}
/**
* Check if 'table' exists in the database
*/
private async $checkIfTableExists(table: string): Promise<boolean> {
const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return rows[0]['COUNT(*)'] === 1;
}
/**
* Get current database version
*/
private async $getSchemaVersionFromDatabase(): Promise<number> {
const connection = await DB.pool.getConnection();
const query = `SELECT number FROM state WHERE name = 'schema_version';`;
const [rows] = await this.$executeQuery(query, true);
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return rows[0]['number'];
}
/**
* Create the `state` table
*/
private async $createMigrationStateTable(): Promise<void> {
try {
const query = `CREATE TABLE IF NOT EXISTS state (
name varchar(25) NOT NULL,
number int(11) NULL,
string varchar(100) NULL,
CONSTRAINT name_unique UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
await this.$executeQuery(query);
// Set initial values
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
await this.$executeQuery(`INSERT INTO state VALUES('last_elements_block', 0, NULL);`);
} catch (e) {
throw e;
}
private async $updateToLatestSchemaVersion(): Promise<void> {
const connection = await DB.pool.getConnection();
const query = `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version'`;
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
}
/**
* We actually execute the migrations queries here
*/
private async $migrateTableSchemaFromVersion(version: number): Promise<void> {
const transactionQueries: string[] = [];
for (const query of this.getMigrationQueriesFromVersion(version)) {
transactionQueries.push(query);
}
transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
try {
await this.$executeQuery('START TRANSACTION;');
for (const query of transactionQueries) {
await this.$executeQuery(query);
}
await this.$executeQuery('COMMIT;');
} catch (e) {
await this.$executeQuery('ROLLBACK;');
throw e;
}
private async $checkIfTableExists(table: string): Promise<boolean> {
const connection = await DB.pool.getConnection();
const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`;
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return rows[0]['COUNT(*)'] === 1;
}
/**
* Generate migration queries based on schema version
*/
private getMigrationQueriesFromVersion(version: number): string[] {
private getInitializeTableQueries(): string[] {
const queries: string[] = [];
const isBitcoin = ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK);
if (version < 1) {
if (config.MEMPOOL.NETWORK !== 'liquid' && config.MEMPOOL.NETWORK !== 'liquidtestnet') {
queries.push(this.getShiftStatisticsQuery());
}
}
if (version < 7 && isBitcoin === true) {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_hashrates_indexing', 0, NULL)`);
}
if (version < 9 && isBitcoin === true) {
queries.push(`INSERT INTO state(name, number, string) VALUES ('last_weekly_hashrates_indexing', 0, NULL)`);
}
return queries;
}
/**
* Save the schema version in the database
*/
private getUpdateToLatestSchemaVersionQuery(): string {
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
}
/**
* Print current database version
*/
private async $printDatabaseVersion() {
try {
const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true);
logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
} catch (e) {
logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
}
}
// Couple of wrappers to clean the main logic
private getShiftStatisticsQuery(): string {
return `UPDATE statistics SET
vsize_1 = vsize_1 + vsize_2, vsize_2 = vsize_3,
vsize_3 = vsize_4, vsize_4 = vsize_5,
vsize_5 = vsize_6, vsize_6 = vsize_8,
vsize_8 = vsize_10, vsize_10 = vsize_12,
vsize_12 = vsize_15, vsize_15 = vsize_20,
vsize_20 = vsize_30, vsize_30 = vsize_40,
vsize_40 = vsize_50, vsize_50 = vsize_60,
vsize_60 = vsize_70, vsize_70 = vsize_80,
vsize_80 = vsize_90, vsize_90 = vsize_100,
vsize_100 = vsize_125, vsize_125 = vsize_150,
vsize_150 = vsize_175, vsize_175 = vsize_200,
vsize_200 = vsize_250, vsize_250 = vsize_300,
vsize_300 = vsize_350, vsize_350 = vsize_400,
vsize_400 = vsize_500, vsize_500 = vsize_600,
vsize_600 = vsize_700, vsize_700 = vsize_800,
vsize_800 = vsize_900, vsize_900 = vsize_1000,
vsize_1000 = vsize_1200, vsize_1200 = vsize_1400,
vsize_1400 = vsize_1800, vsize_1800 = vsize_2000, vsize_2000 = 0;`;
}
private getCreateStatisticsQuery(): string {
return `CREATE TABLE IF NOT EXISTS statistics (
id int(11) NOT NULL AUTO_INCREMENT,
queries.push(`CREATE TABLE IF NOT EXISTS statistics (
id int(11) NOT NULL,
added datetime NOT NULL,
unconfirmed_transactions int(11) UNSIGNED NOT NULL,
tx_per_second float UNSIGNED NOT NULL,
@@ -409,94 +114,64 @@ class DatabaseMigration {
vsize_1400 int(11) NOT NULL,
vsize_1600 int(11) NOT NULL,
vsize_1800 int(11) NOT NULL,
vsize_2000 int(11) NOT NULL,
CONSTRAINT PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
vsize_2000 int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`);
queries.push(`ALTER TABLE statistics ADD PRIMARY KEY (id);`);
queries.push(`ALTER TABLE statistics MODIFY id int(11) NOT NULL AUTO_INCREMENT;`);
return queries;
}
private getCreateElementsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS elements_pegs (
block int(11) NOT NULL,
datetime int(11) NOT NULL,
amount bigint(20) NOT NULL,
txid varchar(65) NOT NULL,
txindex int(11) NOT NULL,
bitcoinaddress varchar(100) NOT NULL,
bitcointxid varchar(65) NOT NULL,
bitcoinindex int(11) NOT NULL,
final_tx int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getMigrationQueriesFromVersion(version: number): string[] {
const queries: string[] = [];
private getCreatePoolsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS pools (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(50) NOT NULL,
link varchar(255) NOT NULL,
addresses text NOT NULL,
regexes text NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
}
private getCreateBlocksTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS blocks (
height int(11) unsigned NOT NULL,
hash varchar(65) NOT NULL,
blockTimestamp timestamp NOT NULL,
size int(11) unsigned NOT NULL,
weight int(11) unsigned NOT NULL,
tx_count int(11) unsigned NOT NULL,
coinbase_raw text,
difficulty bigint(20) unsigned NOT NULL,
pool_id int(11) DEFAULT -1,
fees double unsigned NOT NULL,
fee_span json NOT NULL,
median_fee double unsigned NOT NULL,
PRIMARY KEY (height),
INDEX (pool_id),
FOREIGN KEY (pool_id) REFERENCES pools (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateDailyStatsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS hashrates (
hashrate_timestamp timestamp NOT NULL,
avg_hashrate double unsigned DEFAULT '0',
pool_id smallint unsigned NULL,
PRIMARY KEY (hashrate_timestamp),
INDEX (pool_id),
FOREIGN KEY (pool_id) REFERENCES pools (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateRatesTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS rates (
height int(10) unsigned NOT NULL,
bisq_rates JSON NOT NULL,
PRIMARY KEY (height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates'];
try {
for (const table of tables) {
if (!allowedTables.includes(table)) {
logger.debug(`Table ${table} cannot to be re-indexed (not allowed)`);
continue;
}
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`);
if (version < 1) {
if (config.MEMPOOL.NETWORK !== 'liquid') {
queries.push(`UPDATE statistics SET
vsize_1 = vsize_1 + vsize_2, vsize_2 = vsize_3,
vsize_3 = vsize_4, vsize_4 = vsize_5,
vsize_5 = vsize_6, vsize_6 = vsize_8,
vsize_8 = vsize_10, vsize_10 = vsize_12,
vsize_12 = vsize_15, vsize_15 = vsize_20,
vsize_20 = vsize_30, vsize_30 = vsize_40,
vsize_40 = vsize_50, vsize_50 = vsize_60,
vsize_60 = vsize_70, vsize_70 = vsize_80,
vsize_80 = vsize_90, vsize_90 = vsize_100,
vsize_100 = vsize_125, vsize_125 = vsize_150,
vsize_150 = vsize_175, vsize_175 = vsize_200,
vsize_200 = vsize_250, vsize_250 = vsize_300,
vsize_300 = vsize_350, vsize_350 = vsize_400,
vsize_400 = vsize_500, vsize_500 = vsize_600,
vsize_600 = vsize_700, vsize_700 = vsize_800,
vsize_800 = vsize_900, vsize_900 = vsize_1000,
vsize_1000 = vsize_1200, vsize_1200 = vsize_1400,
vsize_1400 = vsize_1800, vsize_1800 = vsize_2000, vsize_2000 = 0`);
}
} catch (e) {
logger.warn(`Unable to erase indexed data`);
queries.push(`CREATE TABLE IF NOT EXISTS elements_pegs (
block int(11) NOT NULL,
datetime int(11) NOT NULL,
amount bigint(20) NOT NULL,
txid varchar(65) NOT NULL,
txindex int(11) NOT NULL,
bitcoinaddress varchar(100) NOT NULL,
bitcointxid varchar(65) NOT NULL,
bitcoinindex int(11) NOT NULL,
final_tx int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`);
queries.push(`CREATE TABLE IF NOT EXISTS state (
name varchar(25) NOT NULL,
number int(11) NULL,
string varchar(100) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`);
queries.push(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
queries.push(`INSERT INTO state VALUES('last_elements_block', 0, NULL);`);
}
return queries;
}
}

View File

@@ -1,67 +0,0 @@
import config from '../config';
import { IDifficultyAdjustment } from '../mempool.interfaces';
import blocks from './blocks';
class DifficultyAdjustmentApi {
constructor() { }
public getDifficultyAdjustment(): IDifficultyAdjustment {
const DATime = blocks.getLastDifficultyAdjustmentTime();
const previousRetarget = blocks.getPreviousDifficultyRetarget();
const blockHeight = blocks.getCurrentBlockHeight();
const blocksCache = blocks.getBlocks();
const latestBlock = blocksCache[blocksCache.length - 1];
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = blockHeight % 2016;
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
const remainingBlocks = 2016 - blocksInEpoch;
const nextRetargetHeight = blockHeight + remainingBlocks;
let difficultyChange = 0;
if (remainingBlocks < 1870) {
if (blocksInEpoch > 0) {
difficultyChange = (600 / (diff / blocksInEpoch) - 1) * 100;
}
if (difficultyChange > 300) {
difficultyChange = 300;
}
if (difficultyChange < -75) {
difficultyChange = -75;
}
}
let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
// therefore the time between blocks will always be below 20 minutes (1200s).
let timeOffset = 0;
if (config.MEMPOOL.NETWORK === 'testnet') {
if (timeAvgMins > 20) {
timeAvgMins = 20;
}
if (now - latestBlock.timestamp + timeAvgMins * 60 > 1200) {
timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000;
}
}
const timeAvg = timeAvgMins * 60 * 1000 ;
const remainingTime = (remainingBlocks * timeAvg) + (now * 1000);
const estimatedRetargetDate = remainingTime + now;
return {
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
previousRetarget,
nextRetargetHeight,
timeAvg,
timeOffset,
};
}
}
export default new DifficultyAdjustmentApi();

View File

@@ -9,8 +9,6 @@ import { TransactionExtended } from '../mempool.interfaces';
import { Common } from './common';
class DiskCache {
private cacheSchemaVersion = 1;
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
@@ -41,7 +39,6 @@ class DiskCache {
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
@@ -60,13 +57,6 @@ class DiskCache {
}
}
wipeCache() {
fs.unlinkSync(DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
}
loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
@@ -77,11 +67,6 @@ class DiskCache {
if (cacheData) {
logger.info('Restoring mempool and blocks data from disk cache');
data = JSON.parse(cacheData);
if (data.cacheSchemaVersion === undefined || data.cacheSchemaVersion !== this.cacheSchemaVersion) {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) {
for (const tx of data.mempoolArray) {
data.mempool[tx.txid] = tx;
@@ -103,14 +88,14 @@ class DiskCache {
}
}
} catch (e) {
logger.info('Error parsing ' + fileName + '. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
logger.debug('Error parsing ' + fileName + '. Skipping.');
}
}
memPool.setMempool(data.mempool);
blocks.setBlocks(data.blocks);
} catch (e) {
logger.warn('Failed to parse mempoool and blocks cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e));
logger.warn('Failed to parse mempoool and blocks cache. Skipping.');
}
}
}

View File

@@ -1,12 +1,12 @@
import config from '../config';
import { MempoolBlock } from '../mempool.interfaces';
import { Common } from './common';
import mempool from './mempool';
import projectedBlocks from './mempool-blocks';
class FeeApi {
constructor() { }
defaultFee = Common.isLiquid() ? 0.1 : 1;
defaultFee = config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1;
public getRecommendedFee() {
const pBlocks = projectedBlocks.getMempoolBlocks();
@@ -18,7 +18,6 @@ class FeeApi {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
'economyFee': minimumFee,
'minimumFee': minimumFee,
};
}
@@ -31,7 +30,6 @@ class FeeApi {
'fastestFee': firstMedianFee,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
'economyFee': Math.min(2 * minimumFee, thirdMedianFee),
'minimumFee': minimumFee,
};
}

View File

@@ -1,39 +1,22 @@
import logger from '../logger';
import * as http from 'http';
import * as https from 'https';
import axios, { AxiosResponse } from 'axios';
import axios 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 conversionRates: IConversionRates = {
'USD': 0
};
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;
}
}
constructor() { }
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();
}
@@ -43,79 +26,17 @@ class FiatConversion {
}
private async updateCurrency(): Promise<void> {
type axiosOptions = {
headers: {
'User-Agent': string
try {
const response = await axios.get('https://price.bisq.wiz.biz/getAllMarketPrices', { timeout: 10000 });
const usd = response.data.data.find((item: any) => item.currencyCode === 'USD');
this.conversionRates = {
'USD': usd.price,
};
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++;
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);
}
} catch (e) {
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
}
}
}

View File

@@ -2,7 +2,7 @@ import { IBitcoinApi } from '../bitcoin/bitcoin-api.interface';
import bitcoinClient from '../bitcoin/bitcoin-client';
import bitcoinSecondClient from '../bitcoin/bitcoin-second-client';
import { Common } from '../common';
import DB from '../../database';
import { DB } from '../../database';
import logger from '../../logger';
class ElementsParser {
@@ -33,8 +33,10 @@ class ElementsParser {
}
public async $getPegDataByMonth(): Promise<any> {
const connection = await DB.pool.getConnection();
const query = `SELECT SUM(amount) AS amount, DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y-%m-01') AS date FROM elements_pegs GROUP BY DATE_FORMAT(FROM_UNIXTIME(datetime), '%Y%m')`;
const [rows] = await DB.query(query);
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
}
@@ -77,6 +79,7 @@ class ElementsParser {
protected async $savePegToDatabase(height: number, blockTime: number, amount: number, txid: string,
txindex: number, bitcoinaddress: string, bitcointxid: string, bitcoinindex: number, final_tx: number): Promise<void> {
const connection = await DB.pool.getConnection();
const query = `INSERT INTO elements_pegs(
block, datetime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
@@ -84,19 +87,24 @@ class ElementsParser {
const params: (string | number)[] = [
height, blockTime, amount, txid, txindex, bitcoinaddress, bitcointxid, bitcoinindex, final_tx
];
await DB.query(query, params);
await connection.query(query, params);
connection.release();
logger.debug(`Saved L-BTC peg from block height #${height} with TXID ${txid}.`);
}
protected async $getLatestBlockHeightFromDatabase(): Promise<number> {
const connection = await DB.pool.getConnection();
const query = `SELECT number FROM state WHERE name = 'last_elements_block'`;
const [rows] = await DB.query(query);
const [rows] = await connection.query<any>(query);
connection.release();
return rows[0]['number'];
}
protected async $saveLatestBlockToDatabase(blockHeight: number) {
const connection = await DB.pool.getConnection();
const query = `UPDATE state SET number = ? WHERE name = 'last_elements_block'`;
await DB.query(query, [blockHeight]);
await connection.query<any>(query, [blockHeight]);
connection.release();
}
}

View File

@@ -1,38 +0,0 @@
import * as fs from 'fs';
import logger from '../../logger';
class Icons {
private static FILE_NAME = './icons.json';
private iconIds: string[] = [];
private icons: { [assetId: string]: string; } = {};
constructor() {}
public loadIcons() {
if (!fs.existsSync(Icons.FILE_NAME)) {
logger.warn(`${Icons.FILE_NAME} does not exist. No Liquid icons loaded.`);
return;
}
const cacheData = fs.readFileSync(Icons.FILE_NAME, 'utf8');
this.icons = JSON.parse(cacheData);
for (const i in this.icons) {
this.iconIds.push(i);
}
logger.debug(`Liquid icons has been loaded.`);
}
public getIconByAssetId(assetId: string): Buffer | undefined {
const icon = this.icons[assetId];
if (icon) {
return Buffer.from(icon, 'base64');
}
}
public getAllIconIds() {
return this.iconIds;
}
}
export default new Icons();

View File

@@ -12,8 +12,8 @@ class LoadingIndicators {
this.progressChangedCallback = fn;
}
public setProgress(name: string, progressPercent: number, rounded: boolean = true) {
const newProgress = rounded === true ? Math.round(progressPercent) : progressPercent;
public setProgress(name: string, progressPercent: number) {
const newProgress = Math.round(progressPercent);
if (newProgress >= 100) {
delete this.loadingIndicators[name];
} else {

View File

@@ -1,11 +1,10 @@
import logger from '../logger';
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import { Common } from './common';
import config from '../config';
class MempoolBlocks {
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
constructor() {}
@@ -26,10 +25,6 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public getMempoolBlockDeltas(): MempoolBlockDelta[] {
return this.mempoolBlockDeltas;
}
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
const latestMempool = memPool;
const memPoolArray: TransactionExtended[] = [];
@@ -71,15 +66,11 @@ class MempoolBlocks {
const time = end - start;
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
this.mempoolBlocks = blocks;
this.mempoolBlockDeltas = deltas;
this.mempoolBlocks = this.calculateMempoolBlocks(memPoolArray);
}
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
{ blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
let blockWeight = 0;
let blockSize = 0;
let transactions: TransactionExtended[] = [];
@@ -99,43 +90,7 @@ class MempoolBlocks {
if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
}
// Calculate change from previous block states
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = [];
let removed: string[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
removed = prevBlocks[i].transactions.map(tx => tx.txid);
} else if (mempoolBlocks[i] && prevBlocks[i]) {
const prevIds = {};
const newIds = {};
prevBlocks[i].transactions.forEach(tx => {
prevIds[tx.txid] = true;
});
mempoolBlocks[i].transactions.forEach(tx => {
newIds[tx.txid] = true;
});
prevBlocks[i].transactions.forEach(tx => {
if (!newIds[tx.txid]) {
removed.push(tx.txid);
}
});
mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) {
added.push(tx);
}
});
}
mempoolBlockDeltas.push({
added,
removed
});
}
return {
blocks: mempoolBlocks,
deltas: mempoolBlockDeltas
};
return mempoolBlocks;
}
private dataToMempoolBlocks(transactions: TransactionExtended[],
@@ -157,7 +112,6 @@ class MempoolBlocks {
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid),
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
};
}
}

View File

@@ -8,15 +8,13 @@ import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import loadingIndicators from './loading-indicators';
import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false;
private mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {};
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined;
@@ -34,17 +32,6 @@ class Mempool {
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
}
/**
* Return true if we should leave resources available for mempool tx caching
*/
public hasPriority(): boolean {
if (this.inSync) {
return false;
} else {
return this.mempoolCacheDelta == -1 || this.mempoolCacheDelta > 25;
}
}
public isInSync(): boolean {
return this.inSync;
}
@@ -113,8 +100,6 @@ class Mempool {
const diff = transactions.length - currentMempoolSize;
const newTransactions: TransactionExtended[] = [];
this.mempoolCacheDelta = Math.abs(diff);
if (!this.inSync) {
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
}
@@ -189,8 +174,6 @@ class Mempool {
loadingIndicators.setProgress('mempool', 100);
}
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
}
@@ -201,17 +184,6 @@ class Mempool {
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) {
// Store replaced transactions
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
}
}
}
private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);

View File

@@ -1,405 +0,0 @@
import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
import BlocksRepository from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository';
import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import { Common } from './common';
import loadingIndicators from './loading-indicators';
import { escape } from 'mysql2';
class Mining {
constructor() {
}
/**
* Get historical block total fee
*/
public async $getHistoricalBlockFees(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFees(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get historical block rewards
*/
public async $getHistoricalBlockRewards(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockRewards(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get historical block fee rates percentiles
*/
public async $getHistoricalBlockFeeRates(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockFeeRates(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get historical block sizes
*/
public async $getHistoricalBlockSizes(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockSizes(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Get historical block weights
*/
public async $getHistoricalBlockWeights(interval: string | null = null): Promise<any> {
return await BlocksRepository.$getHistoricalBlockWeights(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/**
* Generate high level overview of the pool ranks and general stats
*/
public async $getPoolsStats(interval: string | null): Promise<object> {
const poolsStatistics = {};
const poolsInfo: PoolInfo[] = await PoolsRepository.$getPoolsInfo(interval);
const emptyBlocks: any[] = await BlocksRepository.$countEmptyBlocks(null, interval);
const poolsStats: PoolStats[] = [];
let rank = 1;
poolsInfo.forEach((poolInfo: PoolInfo) => {
const emptyBlocksCount = emptyBlocks.filter((emptyCount) => emptyCount.poolId === poolInfo.poolId);
const poolStat: PoolStats = {
poolId: poolInfo.poolId, // mysql row id
name: poolInfo.name,
link: poolInfo.link,
blockCount: poolInfo.blockCount,
rank: rank++,
emptyBlocks: emptyBlocksCount.length > 0 ? emptyBlocksCount[0]['count'] : 0,
slug: poolInfo.slug,
};
poolsStats.push(poolStat);
});
poolsStatistics['pools'] = poolsStats;
const blockCount: number = await BlocksRepository.$blockCount(null, interval);
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
}
return poolsStatistics;
}
/**
* Get all mining pool stats for a pool
*/
public async $getPoolStat(slug: string): Promise<object> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
}
const blockCount: number = await BlocksRepository.$blockCount(pool.id);
const totalBlock: number = await BlocksRepository.$blockCount(null, null);
const blockCount24h: number = await BlocksRepository.$blockCount(pool.id, '24h');
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
let currentEstimatedHashrate = 0;
try {
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
}
return {
pool: pool,
blockCount: {
'all': blockCount,
'24h': blockCount24h,
'1w': blockCount1w,
},
blockShare: {
'all': blockCount / totalBlock,
'24h': blockCount24h / totalBlock24h,
'1w': blockCount1w / totalBlock1w,
},
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
reportedHashrate: null,
};
}
/**
* Get miner reward stats
*/
public async $getRewardStats(blockCount: number): Promise<RewardStats> {
return await BlocksRepository.$getBlockStats(blockCount);
}
/**
* [INDEXING] Generate weekly mining pool hashrate history
*/
public async $generatePoolHashrateHistory(): Promise<void> {
const now = new Date();
try {
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
// Run only if:
// * lastestRunDate is set to 0 (node backend restart, reorg)
// * we started a new week (around Monday midnight)
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
if (!runIndexing) {
return;
}
} catch (e) {
throw e;
}
try {
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
const hashrates: any[] = [];
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
const lastMondayMidnight = this.getDateMidnight(lastMonday);
let toTimestamp = lastMondayMidnight.getTime();
const totalWeekIndexed = (await BlocksRepository.$blockCount(null, null)) / 1008;
let indexedThisRun = 0;
let totalIndexed = 0;
let newlyIndexed = 0;
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
logger.debug(`Indexing weekly mining pool hashrate`);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 604800000;
// Skip already indexed weeks
if (indexedTimestamp.includes(toTimestamp / 1000)) {
toTimestamp -= 604800000;
++totalIndexed;
continue;
}
// Check if we have blocks for the previous week (which mean that the week
// we are currently indexing has complete data)
const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000);
if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
pools = pools.map((pool: any) => {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
pool.share = (pool.blockCount / totalBlocks);
return pool;
});
for (const pool of pools) {
hashrates.push({
hashrateTimestamp: toTimestamp / 1000,
avgHashrate: pool['hashrate'],
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
});
}
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
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 | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
}
toTimestamp -= 604800000;
++indexedThisRun;
++totalIndexed;
}
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
}
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
throw e;
}
}
/**
* [INDEXING] Generate daily hashrate data
*/
public async $generateNetworkHashrateHistory(): Promise<void> {
try {
// We only run this once a day around midnight
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
const now = new Date().getUTCDate();
if (now === latestRunDate) {
return;
}
} catch (e) {
throw e;
}
try {
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMidnight = this.getDateMidnight(new Date());
let toTimestamp = Math.round(lastMidnight.getTime());
const hashrates: any[] = [];
const totalDayIndexed = (await BlocksRepository.$blockCount(null, null)) / 144;
let indexedThisRun = 0;
let totalIndexed = 0;
let newlyIndexed = 0;
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
logger.debug(`Indexing daily network hashrate`);
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400000;
// Skip already indexed weeks
if (indexedTimestamp.includes(toTimestamp / 1000)) {
toTimestamp -= 86400000;
++totalIndexed;
continue;
}
// Check if we have blocks for the previous day (which mean that the day
// we are currently indexing has complete data)
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
if (blockStatsPreviousDay.blockCount === 0) { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight);
hashrates.push({
hashrateTimestamp: toTimestamp / 1000,
avgHashrate: lastBlockHashrate,
poolId: 0,
share: 1,
type: 'daily',
});
if (hashrates.length > 10) {
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
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 | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
}
toTimestamp -= 86400000;
++indexedThisRun;
++totalIndexed;
}
// Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
hashrates.push({
hashrateTimestamp: genesisTimestamp,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null,
type: 'daily',
});
}
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
}
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
throw e;
}
}
private getDateMidnight(date: Date): Date {
date.setUTCHours(0);
date.setUTCMinutes(0);
date.setUTCSeconds(0);
date.setUTCMilliseconds(0);
return date;
}
private getTimeRange(interval: string | null): number {
switch (interval) {
case '3y': return 43200; // 12h
case '2y': return 28800; // 8h
case '1y': return 28800; // 8h
case '6m': return 10800; // 3h
case '3m': return 7200; // 2h
case '1m': return 1800; // 30min
case '1w': return 300; // 5min
case '3d': return 1;
case '24h': return 1;
default: return 86400; // 24h
}
}
}
export default new Mining();

View File

@@ -1,206 +0,0 @@
import DB from '../database';
import logger from '../logger';
import config from '../config';
interface Pool {
name: string;
link: string;
regexes: string[];
addresses: string[];
slug: string;
}
class PoolsParser {
miningPools: any[] = [];
unknownPool: any = {
'name': "Unknown",
'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
'regexes': "[]",
'addresses': "[]",
'slug': 'unknown'
};
slugWarnFlag = false;
/**
* Parse the pools.json file, consolidate the data and dump it into the database
*/
public async migratePoolsJson(poolsJson: object) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return;
}
// First we save every entries without paying attention to pool duplication
const poolsDuplicated: Pool[] = [];
logger.debug('Parse coinbase_tags');
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
for (let i = 0; i < coinbaseTags.length; ++i) {
poolsDuplicated.push({
'name': (<Pool>coinbaseTags[i][1]).name,
'link': (<Pool>coinbaseTags[i][1]).link,
'regexes': [coinbaseTags[i][0]],
'addresses': [],
'slug': ''
});
}
logger.debug('Parse payout_addresses');
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
logger.debug('Identify unique mining pools');
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`);
// 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');
return;
}
this.miningPools = [];
// Finally, we generate the final consolidated pools data
const finalPoolDataAdd: Pool[] = [];
const finalPoolDataUpdate: 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 finalPoolName = poolNames[i].replace(`'`, `''`); // To support single quote in names when doing db queries
let slug: string | undefined;
try {
slug = poolsJson['slugs'][poolNames[i]];
} catch (e) {
if (this.slugWarnFlag === false) {
logger.warn(`pools.json does not seem to contain the 'slugs' object`);
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}'`);
}
const poolObj = {
'name': finalPoolName,
'link': match[0].link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
};
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
finalPoolDataUpdate.push(poolObj);
} else {
logger.debug(`Add '${finalPoolName}' mining pool`);
finalPoolDataAdd.push(poolObj);
}
this.miningPools.push({
'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)');
return;
}
logger.debug(`Update pools table now`);
// 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}'
;`);
}
try {
if (finalPoolDataAdd.length > 0) {
await DB.query({ sql: queryAdd, timeout: 120000 });
}
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool();
logger.info('Mining pools.json import completed');
} catch (e) {
logger.err(`Cannot import pools in the database`);
throw e;
}
}
/**
* Manually add the 'unknown pool'
*/
private async insertUnknownPool() {
try {
const [rows]: any[] = await DB.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 });
if (rows.length === 0) {
await DB.query({
sql: `INSERT INTO pools(name, link, regexes, addresses, slug)
VALUES("Unknown", "https://learnmeabitcoin.com/technical/coinbase-transaction", "[]", "[]", "unknown");
`});
} else {
await DB.query(`UPDATE pools
SET name='Unknown', link='https://learnmeabitcoin.com/technical/coinbase-transaction',
regexes='[]', addresses='[]',
slug='unknown'
WHERE name='Unknown'
`);
}
} catch (e) {
logger.err('Unable to insert "Unknown" mining pool');
}
}
}
export default new PoolsParser();

View File

@@ -1,34 +0,0 @@
export interface CachedRbf {
txid: string;
expires: Date;
}
class RbfCache {
private cache: { [txid: string]: CachedRbf; } = {};
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
}
public add(replacedTxId: string, newTxId: string): void {
this.cache[replacedTxId] = {
expires: new Date(Date.now() + 1000 * 604800), // 1 week
txid: newTxId,
};
}
public get(txId: string): CachedRbf | undefined {
return this.cache[txId];
}
private cleanup(): void {
const currentDate = new Date();
for (const c in this.cache) {
if (this.cache[c].expires < currentDate) {
delete this.cache[c];
}
}
}
}
export default new RbfCache();

View File

@@ -1,15 +1,17 @@
import memPool from './mempool';
import DB from '../database';
import { DB } from '../database';
import logger from '../logger';
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
import config from '../config';
import { Common } from './common';
class Statistics {
protected intervalTimer: NodeJS.Timer | undefined;
protected newStatisticsEntryCallback: ((stats: OptimizedStatistic) => void) | undefined;
protected queryTimeout = 120000;
protected cache: { [date: string]: OptimizedStatistic[] } = {
'24h': [], '1w': [], '1m': [], '3m': [], '6m': [], '1y': [], '2y': [], '3y': []
};
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
this.newStatisticsEntryCallback = fn;
@@ -31,6 +33,25 @@ class Statistics {
this.runStatistics();
}, 1 * 60 * 1000);
}, difference);
this.createCache();
setInterval(this.createCache.bind(this), 600000);
}
public getCache() {
return this.cache;
}
private async createCache() {
this.cache['24h'] = await this.$list24H();
this.cache['1w'] = await this.$list1W();
this.cache['1m'] = await this.$list1M();
this.cache['3m'] = await this.$list3M();
this.cache['6m'] = await this.$list6M();
this.cache['1y'] = await this.$list1Y();
this.cache['2y'] = await this.$list2Y();
this.cache['3y'] = await this.$list3Y();
logger.debug('Statistics cache created');
}
private async runStatistics(): Promise<void> {
@@ -53,17 +74,6 @@ class Statistics {
memPoolArray = memPoolArray.filter((tx) => tx.effectiveFeePerVsize);
if (!memPoolArray.length) {
try {
const insertIdZeroed = await this.$createZeroedStatistic();
if (this.newStatisticsEntryCallback && insertIdZeroed) {
const newStats = await this.$get(insertIdZeroed);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
}
} catch (e) {
logger.err('Unable to insert zeroed statistics. ' + e);
}
return;
}
@@ -80,9 +90,9 @@ class Statistics {
memPoolArray.forEach((transaction) => {
for (let i = 0; i < logFees.length; i++) {
if (
(Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1]))
(config.MEMPOOL.NETWORK === 'liquid' && (i === lastItem || transaction.effectiveFeePerVsize * 10 < logFees[i + 1]))
||
(!Common.isLiquid() && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1]))
(config.MEMPOOL.NETWORK !== 'liquid' && (i === lastItem || transaction.effectiveFeePerVsize < logFees[i + 1]))
) {
if (weightVsizeFees[logFees[i]]) {
weightVsizeFees[logFees[i]] += transaction.vsize;
@@ -94,126 +104,65 @@ class Statistics {
}
});
try {
const insertId = await this.$create({
added: 'NOW()',
unconfirmed_transactions: memPoolArray.length,
tx_per_second: txPerSecond,
vbytes_per_second: Math.round(vBytesPerSecond),
mempool_byte_weight: totalWeight,
total_fee: totalFee,
fee_data: '',
vsize_1: weightVsizeFees['1'] || 0,
vsize_2: weightVsizeFees['2'] || 0,
vsize_3: weightVsizeFees['3'] || 0,
vsize_4: weightVsizeFees['4'] || 0,
vsize_5: weightVsizeFees['5'] || 0,
vsize_6: weightVsizeFees['6'] || 0,
vsize_8: weightVsizeFees['8'] || 0,
vsize_10: weightVsizeFees['10'] || 0,
vsize_12: weightVsizeFees['12'] || 0,
vsize_15: weightVsizeFees['15'] || 0,
vsize_20: weightVsizeFees['20'] || 0,
vsize_30: weightVsizeFees['30'] || 0,
vsize_40: weightVsizeFees['40'] || 0,
vsize_50: weightVsizeFees['50'] || 0,
vsize_60: weightVsizeFees['60'] || 0,
vsize_70: weightVsizeFees['70'] || 0,
vsize_80: weightVsizeFees['80'] || 0,
vsize_90: weightVsizeFees['90'] || 0,
vsize_100: weightVsizeFees['100'] || 0,
vsize_125: weightVsizeFees['125'] || 0,
vsize_150: weightVsizeFees['150'] || 0,
vsize_175: weightVsizeFees['175'] || 0,
vsize_200: weightVsizeFees['200'] || 0,
vsize_250: weightVsizeFees['250'] || 0,
vsize_300: weightVsizeFees['300'] || 0,
vsize_350: weightVsizeFees['350'] || 0,
vsize_400: weightVsizeFees['400'] || 0,
vsize_500: weightVsizeFees['500'] || 0,
vsize_600: weightVsizeFees['600'] || 0,
vsize_700: weightVsizeFees['700'] || 0,
vsize_800: weightVsizeFees['800'] || 0,
vsize_900: weightVsizeFees['900'] || 0,
vsize_1000: weightVsizeFees['1000'] || 0,
vsize_1200: weightVsizeFees['1200'] || 0,
vsize_1400: weightVsizeFees['1400'] || 0,
vsize_1600: weightVsizeFees['1600'] || 0,
vsize_1800: weightVsizeFees['1800'] || 0,
vsize_2000: weightVsizeFees['2000'] || 0,
});
const insertId = await this.$create({
added: 'NOW()',
unconfirmed_transactions: memPoolArray.length,
tx_per_second: txPerSecond,
vbytes_per_second: Math.round(vBytesPerSecond),
mempool_byte_weight: totalWeight,
total_fee: totalFee,
fee_data: '',
vsize_1: weightVsizeFees['1'] || 0,
vsize_2: weightVsizeFees['2'] || 0,
vsize_3: weightVsizeFees['3'] || 0,
vsize_4: weightVsizeFees['4'] || 0,
vsize_5: weightVsizeFees['5'] || 0,
vsize_6: weightVsizeFees['6'] || 0,
vsize_8: weightVsizeFees['8'] || 0,
vsize_10: weightVsizeFees['10'] || 0,
vsize_12: weightVsizeFees['12'] || 0,
vsize_15: weightVsizeFees['15'] || 0,
vsize_20: weightVsizeFees['20'] || 0,
vsize_30: weightVsizeFees['30'] || 0,
vsize_40: weightVsizeFees['40'] || 0,
vsize_50: weightVsizeFees['50'] || 0,
vsize_60: weightVsizeFees['60'] || 0,
vsize_70: weightVsizeFees['70'] || 0,
vsize_80: weightVsizeFees['80'] || 0,
vsize_90: weightVsizeFees['90'] || 0,
vsize_100: weightVsizeFees['100'] || 0,
vsize_125: weightVsizeFees['125'] || 0,
vsize_150: weightVsizeFees['150'] || 0,
vsize_175: weightVsizeFees['175'] || 0,
vsize_200: weightVsizeFees['200'] || 0,
vsize_250: weightVsizeFees['250'] || 0,
vsize_300: weightVsizeFees['300'] || 0,
vsize_350: weightVsizeFees['350'] || 0,
vsize_400: weightVsizeFees['400'] || 0,
vsize_500: weightVsizeFees['500'] || 0,
vsize_600: weightVsizeFees['600'] || 0,
vsize_700: weightVsizeFees['700'] || 0,
vsize_800: weightVsizeFees['800'] || 0,
vsize_900: weightVsizeFees['900'] || 0,
vsize_1000: weightVsizeFees['1000'] || 0,
vsize_1200: weightVsizeFees['1200'] || 0,
vsize_1400: weightVsizeFees['1400'] || 0,
vsize_1600: weightVsizeFees['1600'] || 0,
vsize_1800: weightVsizeFees['1800'] || 0,
vsize_2000: weightVsizeFees['2000'] || 0,
});
if (this.newStatisticsEntryCallback && insertId) {
const newStats = await this.$get(insertId);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
if (this.newStatisticsEntryCallback && insertId) {
const newStats = await this.$get(insertId);
if (newStats) {
this.newStatisticsEntryCallback(newStats);
}
} catch (e) {
logger.err('Unable to insert statistics. ' + e);
}
}
private async $createZeroedStatistic(): Promise<number | undefined> {
try {
const query = `INSERT INTO statistics(
added,
unconfirmed_transactions,
tx_per_second,
vbytes_per_second,
mempool_byte_weight,
fee_data,
total_fee,
vsize_1,
vsize_2,
vsize_3,
vsize_4,
vsize_5,
vsize_6,
vsize_8,
vsize_10,
vsize_12,
vsize_15,
vsize_20,
vsize_30,
vsize_40,
vsize_50,
vsize_60,
vsize_70,
vsize_80,
vsize_90,
vsize_100,
vsize_125,
vsize_150,
vsize_175,
vsize_200,
vsize_250,
vsize_300,
vsize_350,
vsize_400,
vsize_500,
vsize_600,
vsize_700,
vsize_800,
vsize_900,
vsize_1000,
vsize_1200,
vsize_1400,
vsize_1600,
vsize_1800,
vsize_2000
)
VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`;
const [result]: any = await DB.query(query);
return result.insertId;
} catch (e) {
logger.err('$create() error' + (e instanceof Error ? e.message : e));
}
}
private async $create(statistics: Statistic): Promise<number | undefined> {
try {
const connection = await DB.pool.getConnection();
const query = `INSERT INTO statistics(
added,
unconfirmed_transactions,
@@ -310,7 +259,8 @@ class Statistics {
statistics.vsize_1800,
statistics.vsize_2000,
];
const [result]: any = await DB.query(query, params);
const [result]: any = await connection.query(query, params);
connection.release();
return result.insertId;
} catch (e) {
logger.err('$create() error' + (e instanceof Error ? e.message : e));
@@ -318,57 +268,58 @@ class Statistics {
}
private getQueryForDaysAvg(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
CAST(avg(vsize_1) as DOUBLE) as vsize_1,
CAST(avg(vsize_2) as DOUBLE) as vsize_2,
CAST(avg(vsize_3) as DOUBLE) as vsize_3,
CAST(avg(vsize_4) as DOUBLE) as vsize_4,
CAST(avg(vsize_5) as DOUBLE) as vsize_5,
CAST(avg(vsize_6) as DOUBLE) as vsize_6,
CAST(avg(vsize_8) as DOUBLE) as vsize_8,
CAST(avg(vsize_10) as DOUBLE) as vsize_10,
CAST(avg(vsize_12) as DOUBLE) as vsize_12,
CAST(avg(vsize_15) as DOUBLE) as vsize_15,
CAST(avg(vsize_20) as DOUBLE) as vsize_20,
CAST(avg(vsize_30) as DOUBLE) as vsize_30,
CAST(avg(vsize_40) as DOUBLE) as vsize_40,
CAST(avg(vsize_50) as DOUBLE) as vsize_50,
CAST(avg(vsize_60) as DOUBLE) as vsize_60,
CAST(avg(vsize_70) as DOUBLE) as vsize_70,
CAST(avg(vsize_80) as DOUBLE) as vsize_80,
CAST(avg(vsize_90) as DOUBLE) as vsize_90,
CAST(avg(vsize_100) as DOUBLE) as vsize_100,
CAST(avg(vsize_125) as DOUBLE) as vsize_125,
CAST(avg(vsize_150) as DOUBLE) as vsize_150,
CAST(avg(vsize_175) as DOUBLE) as vsize_175,
CAST(avg(vsize_200) as DOUBLE) as vsize_200,
CAST(avg(vsize_250) as DOUBLE) as vsize_250,
CAST(avg(vsize_300) as DOUBLE) as vsize_300,
CAST(avg(vsize_350) as DOUBLE) as vsize_350,
CAST(avg(vsize_400) as DOUBLE) as vsize_400,
CAST(avg(vsize_500) as DOUBLE) as vsize_500,
CAST(avg(vsize_600) as DOUBLE) as vsize_600,
CAST(avg(vsize_700) as DOUBLE) as vsize_700,
CAST(avg(vsize_800) as DOUBLE) as vsize_800,
CAST(avg(vsize_900) as DOUBLE) as vsize_900,
CAST(avg(vsize_1000) as DOUBLE) as vsize_1000,
CAST(avg(vsize_1200) as DOUBLE) as vsize_1200,
CAST(avg(vsize_1400) as DOUBLE) as vsize_1400,
CAST(avg(vsize_1600) as DOUBLE) as vsize_1600,
CAST(avg(vsize_1800) as DOUBLE) as vsize_1800,
CAST(avg(vsize_2000) as DOUBLE) as vsize_2000 \
return `SELECT id, UNIX_TIMESTAMP(added) as added,
CAST(avg(unconfirmed_transactions) as FLOAT) as unconfirmed_transactions,
CAST(avg(tx_per_second) as FLOAT) as tx_per_second,
CAST(avg(vbytes_per_second) as FLOAT) as vbytes_per_second,
CAST(avg(vsize_1) as FLOAT) as vsize_1,
CAST(avg(vsize_2) as FLOAT) as vsize_2,
CAST(avg(vsize_3) as FLOAT) as vsize_3,
CAST(avg(vsize_4) as FLOAT) as vsize_4,
CAST(avg(vsize_5) as FLOAT) as vsize_5,
CAST(avg(vsize_6) as FLOAT) as vsize_6,
CAST(avg(vsize_8) as FLOAT) as vsize_8,
CAST(avg(vsize_10) as FLOAT) as vsize_10,
CAST(avg(vsize_12) as FLOAT) as vsize_12,
CAST(avg(vsize_15) as FLOAT) as vsize_15,
CAST(avg(vsize_20) as FLOAT) as vsize_20,
CAST(avg(vsize_30) as FLOAT) as vsize_30,
CAST(avg(vsize_40) as FLOAT) as vsize_40,
CAST(avg(vsize_50) as FLOAT) as vsize_50,
CAST(avg(vsize_60) as FLOAT) as vsize_60,
CAST(avg(vsize_70) as FLOAT) as vsize_70,
CAST(avg(vsize_80) as FLOAT) as vsize_80,
CAST(avg(vsize_90) as FLOAT) as vsize_90,
CAST(avg(vsize_100) as FLOAT) as vsize_100,
CAST(avg(vsize_125) as FLOAT) as vsize_125,
CAST(avg(vsize_150) as FLOAT) as vsize_150,
CAST(avg(vsize_175) as FLOAT) as vsize_175,
CAST(avg(vsize_200) as FLOAT) as vsize_200,
CAST(avg(vsize_250) as FLOAT) as vsize_250,
CAST(avg(vsize_300) as FLOAT) as vsize_300,
CAST(avg(vsize_350) as FLOAT) as vsize_350,
CAST(avg(vsize_400) as FLOAT) as vsize_400,
CAST(avg(vsize_500) as FLOAT) as vsize_500,
CAST(avg(vsize_600) as FLOAT) as vsize_600,
CAST(avg(vsize_700) as FLOAT) as vsize_700,
CAST(avg(vsize_800) as FLOAT) as vsize_800,
CAST(avg(vsize_900) as FLOAT) as vsize_900,
CAST(avg(vsize_1000) as FLOAT) as vsize_1000,
CAST(avg(vsize_1200) as FLOAT) as vsize_1200,
CAST(avg(vsize_1400) as FLOAT) as vsize_1400,
CAST(avg(vsize_1600) as FLOAT) as vsize_1600,
CAST(avg(vsize_1800) as FLOAT) as vsize_1800,
CAST(avg(vsize_2000) as FLOAT) as vsize_2000 \
FROM statistics \
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
ORDER BY statistics.added DESC;`;
ORDER BY id DESC;`;
}
private getQueryForDays(div: number, interval: string) {
return `SELECT
UNIX_TIMESTAMP(added) as added,
CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second,
return `SELECT id, UNIX_TIMESTAMP(added) as added, unconfirmed_transactions,
tx_per_second,
vbytes_per_second,
vsize_1,
vsize_2,
vsize_3,
@@ -410,13 +361,15 @@ class Statistics {
FROM statistics \
WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW() \
GROUP BY UNIX_TIMESTAMP(added) DIV ${div} \
ORDER BY statistics.added DESC;`;
ORDER BY id DESC;`;
}
private async $get(id: number): Promise<OptimizedStatistic | undefined> {
public async $get(id: number): Promise<OptimizedStatistic | undefined> {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics WHERE id = ?`;
const [rows] = await DB.query(query, [id]);
const [rows] = await connection.query<any>(query, [id]);
connection.release();
if (rows[0]) {
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
}
@@ -427,9 +380,11 @@ class Statistics {
public async $list2H(): Promise<OptimizedStatistic[]> {
try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 120`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY id DESC LIMIT 120`;
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list2H() error' + (e instanceof Error ? e.message : e));
return [];
@@ -438,9 +393,11 @@ class Statistics {
public async $list24H(): Promise<OptimizedStatistic[]> {
try {
const query = `SELECT *, UNIX_TIMESTAMP(added) as added FROM statistics ORDER BY statistics.added DESC LIMIT 1440`;
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDaysAvg(120, '1 DAY'); // 2m interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list24h() error' + (e instanceof Error ? e.message : e));
return [];
@@ -449,9 +406,11 @@ class Statistics {
public async $list1W(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDaysAvg(300, '1 WEEK'); // 5m interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDaysAvg(600, '1 WEEK'); // 10m interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list1W() error' + (e instanceof Error ? e.message : e));
return [];
@@ -460,9 +419,11 @@ class Statistics {
public async $list1M(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDaysAvg(1800, '1 MONTH'); // 30m interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDaysAvg(3600, '1 MONTH'); // 1h interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list1M() error' + (e instanceof Error ? e.message : e));
return [];
@@ -471,9 +432,11 @@ class Statistics {
public async $list3M(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDaysAvg(7200, '3 MONTH'); // 2h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDaysAvg(14400, '3 MONTH'); // 4h interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list3M() error' + (e instanceof Error ? e.message : e));
return [];
@@ -482,9 +445,11 @@ class Statistics {
public async $list6M(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDaysAvg(10800, '6 MONTH'); // 3h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDaysAvg(21600, '6 MONTH'); // 6h interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list6M() error' + (e instanceof Error ? e.message : e));
return [];
@@ -493,9 +458,11 @@ class Statistics {
public async $list1Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(28800, '1 YEAR'); // 8h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(43200, '1 YEAR'); // 12h interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list1Y() error' + (e instanceof Error ? e.message : e));
return [];
@@ -504,9 +471,11 @@ class Statistics {
public async $list2Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(28800, '2 YEAR'); // 8h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(86400, "2 YEAR"); // 1d interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list2Y() error' + (e instanceof Error ? e.message : e));
return [];
@@ -515,9 +484,11 @@ class Statistics {
public async $list3Y(): Promise<OptimizedStatistic[]> {
try {
const query = this.getQueryForDays(43200, '3 YEAR'); // 12h interval
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
const connection = await DB.pool.getConnection();
const query = this.getQueryForDays(86400, "3 YEAR"); // 1d interval
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
connection.release();
return this.mapStatisticToOptimizedStatistic(rows);
} catch (e) {
logger.err('$list3Y() error' + (e instanceof Error ? e.message : e));
return [];
@@ -527,7 +498,10 @@ class Statistics {
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
return statistic.map((s) => {
return {
id: s.id || 0,
added: s.added,
unconfirmed_transactions: s.unconfirmed_transactions,
tx_per_second: s.tx_per_second,
vbytes_per_second: s.vbytes_per_second,
mempool_byte_weight: s.mempool_byte_weight,
total_fee: s.total_fee,

View File

@@ -2,7 +2,6 @@ import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import config from '../config';
import { Common } from './common';
class TransactionUtils {
constructor() { }
@@ -21,8 +20,8 @@ class TransactionUtils {
};
}
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise<TransactionExtended> {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
public async $getTransactionExtended(txId: string, addPrevouts = false): Promise<TransactionExtended> {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
return this.extendTransaction(transaction);
}
@@ -32,8 +31,7 @@ class TransactionUtils {
// @ts-ignore
return transaction;
}
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / (transaction.weight / 4));
const feePerVbytes = Math.max(config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1, (transaction.fee || 0) / (transaction.weight / 4));
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: feePerVbytes,
@@ -44,14 +42,6 @@ class TransactionUtils {
}
return transactionExtended;
}
public hex2ascii(hex: string) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
}
export default new TransactionUtils();

View File

@@ -1,6 +1,6 @@
import logger from '../logger';
import * as WebSocket from 'ws';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -11,9 +11,6 @@ import { Common } from './common';
import loadingIndicators from './loading-indicators';
import config from '../config';
import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -51,38 +48,29 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-tx']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-tx'])) {
client['track-tx'] = parsedMessage['track-tx'];
// Client is telling the transaction wasn't found
// Client is telling the transaction wasn't found but it might have appeared before we had the time to start watching for it
if (parsedMessage['watch-mempool']) {
const rbfCacheTx = rbfCache.get(client['track-tx']);
if (rbfCacheTx) {
response['txReplaced'] = {
txid: rbfCacheTx.txid,
};
client['track-tx'] = null;
} else {
// It might have appeared before we had the time to start watching for it
const tx = memPool.getMempool()[client['track-tx']];
if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx;
} else {
// tx.prevout is missing from transactions when in bitcoind mode
try {
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
}
}
const tx = memPool.getMempool()[client['track-tx']];
if (tx) {
if (config.MEMPOOL.BACKEND === 'esplora') {
response['tx'] = tx;
} else {
// tx.prevouts is missing from transactions when in bitcoind mode
try {
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
client['track-mempool-tx'] = parsedMessage['track-tx'];
logger.debug('Error finding transaction: ' + (e instanceof Error ? e.message : e));
}
}
} else {
try {
const fullTx = await transactionUtils.$getTransactionExtended(client['track-tx'], true);
response['tx'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction. ' + (e instanceof Error ? e.message : e));
client['track-mempool-tx'] = parsedMessage['track-tx'];
}
}
}
} else {
@@ -111,20 +99,6 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = {
index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
};
} else {
client['track-mempool-block'] = null;
}
}
if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
if (!_blocks) {
@@ -207,14 +181,14 @@ class WebsocketHandler {
return {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
'blocks': _blocks,
'conversions': fiatConversion.getConversionRates(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
'transactions': memPool.getLatestTransactions(),
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties
};
}
@@ -247,13 +221,14 @@ class WebsocketHandler {
mempoolBlocks.updateMempoolBlocks(newMempool);
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempool = memPool.getMempool();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions);
const recommendedFees = feeApi.getRecommendedFee();
for (const rbfTransaction in rbfTransactions) {
delete mempool[rbfTransaction];
}
this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
@@ -266,8 +241,6 @@ class WebsocketHandler {
response['mempoolInfo'] = mempoolInfo;
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
response['da'] = da;
response['fees'] = recommendedFees;
}
if (client['want-mempool-blocks']) {
@@ -358,43 +331,25 @@ class WebsocketHandler {
}
}
if (client['track-tx']) {
const outspends: object = {};
newTransactions.forEach((tx) => tx.vin.forEach((vin, i) => {
if (vin.txid === client['track-tx']) {
outspends[vin.vout] = {
vin: i,
txid: tx.txid,
};
}
}));
if (Object.keys(outspends).length) {
response['utxoSpent'] = outspends;
}
if (rbfTransactions[client['track-tx']]) {
for (const rbfTransaction in rbfTransactions) {
if (client['track-tx'] === rbfTransaction) {
response['rbfTransaction'] = {
txid: rbfTransactions[rbfTransaction].txid,
};
break;
if (client['track-tx'] && rbfTransactions[client['track-tx']]) {
for (const rbfTransaction in rbfTransactions) {
if (client['track-tx'] === rbfTransaction) {
const rbfTx = rbfTransactions[rbfTransaction];
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true);
response['rbfTransaction'] = fullTx;
} catch (e) {
logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
}
} else {
response['rbfTransaction'] = rbfTx;
}
break;
}
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
}
@@ -407,7 +362,6 @@ class WebsocketHandler {
}
let mBlocks: undefined | MempoolBlock[];
let mBlockDeltas: undefined | MempoolBlockDelta[];
let matchRate = 0;
const _memPool = memPool.getMempool();
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
@@ -424,15 +378,9 @@ class WebsocketHandler {
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
}
if (block.extras) {
block.extras.matchRate = matchRate;
}
const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee();
block.matchRate = matchRate;
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
@@ -446,8 +394,8 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'da': da,
'fees': fees,
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
'previousRetarget': blocks.getPreviousDifficultyRetarget(),
};
if (mBlocks && client['want-mempool-blocks']) {
@@ -455,6 +403,7 @@ class WebsocketHandler {
}
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
client['track-tx'] = null;
response['txConfirmed'] = true;
}
@@ -522,16 +471,6 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
client.send(JSON.stringify(response));
});
}

View File

@@ -2,7 +2,7 @@ const configFile = require('../mempool-config.json');
interface IConfig {
MEMPOOL: {
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid' | 'liquidtestnet';
NETWORK: 'mainnet' | 'testnet' | 'signet' | 'liquid';
BACKEND: 'esplora' | 'electrum' | 'none';
HTTP_PORT: number;
SPAWN_CLUSTER_PROCS: number;
@@ -14,14 +14,8 @@ interface IConfig {
BLOCK_WEIGHT_UNITS: number;
INITIAL_BLOCKS_AMOUNT: number;
MEMPOOL_BLOCKS_AMOUNT: number;
INDEXING_BLOCKS_AMOUNT: number;
PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[];
EXTERNAL_MAX_RETRY: number;
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
};
ESPLORA: {
REST_API_URL: string;
@@ -46,7 +40,6 @@ interface IConfig {
DATABASE: {
ENABLED: boolean;
HOST: string,
SOCKET: string,
PORT: number;
DATABASE: string;
USERNAME: string;
@@ -56,7 +49,7 @@ interface IConfig {
ENABLED: boolean;
HOST: string;
PORT: number;
MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' |'warn' | 'notice' | 'info' | 'debug';
FACILITY: string;
};
STATISTICS: {
@@ -67,26 +60,6 @@ interface IConfig {
ENABLED: boolean;
DATA_PATH: string;
};
SOCKS5PROXY: {
ENABLED: boolean;
USE_ONION: boolean;
HOST: string;
PORT: number;
USERNAME: string;
PASSWORD: string;
};
PRICE_DATA_SERVER: {
TOR_URL: string;
CLEARNET_URL: string;
};
EXTERNAL_DATA_SERVER: {
MEMPOOL_API: string;
MEMPOOL_ONION: string;
LIQUID_API: string;
LIQUID_ONION: string;
BISQ_URL: string;
BISQ_ONION: string;
};
}
const defaults: IConfig = {
@@ -103,14 +76,8 @@ const defaults: IConfig = {
'BLOCK_WEIGHT_UNITS': 4000000,
'INITIAL_BLOCKS_AMOUNT': 8,
'MEMPOOL_BLOCKS_AMOUNT': 8,
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
'PRICE_FEED_UPDATE_INTERVAL': 600,
'PRICE_FEED_UPDATE_INTERVAL': 3600,
'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [],
'EXTERNAL_MAX_RETRY': 1,
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
@@ -135,7 +102,6 @@ const defaults: IConfig = {
'DATABASE': {
'ENABLED': true,
'HOST': '127.0.0.1',
'SOCKET': '',
'PORT': 3306,
'DATABASE': 'mempool',
'USERNAME': 'mempool',
@@ -156,26 +122,6 @@ const defaults: IConfig = {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
'SOCKS5PROXY': {
'ENABLED': false,
'USE_ONION': true,
'HOST': '127.0.0.1',
'PORT': 9050,
'USERNAME': '',
'PASSWORD': ''
},
"PRICE_DATA_SERVER": {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
"EXTERNAL_DATA_SERVER": {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
'LIQUID_API': 'https://liquid.network/api/v1',
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
'BISQ_URL': 'https://bisq.markets/api',
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}
};
class Config implements IConfig {
@@ -188,9 +134,6 @@ class Config implements IConfig {
SYSLOG: IConfig['SYSLOG'];
STATISTICS: IConfig['STATISTICS'];
BISQ: IConfig['BISQ'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
constructor() {
const configs = this.merge(configFile, defaults);
@@ -203,9 +146,6 @@ class Config implements IConfig {
this.SYSLOG = configs.SYSLOG;
this.STATISTICS = configs.STATISTICS;
this.BISQ = configs.BISQ;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -1,59 +1,26 @@
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { createPool } from 'mysql2/promise';
import logger from './logger';
import { PoolOptions } from 'mysql2/typings/mysql';
class DB {
constructor() {
if (config.DATABASE.SOCKET !== '') {
this.poolConfig.socketPath = config.DATABASE.SOCKET;
} else {
this.poolConfig.host = config.DATABASE.HOST;
}
}
private pool: Pool | null = null;
private poolConfig: PoolOptions = {
export class DB {
static pool = createPool({
host: config.DATABASE.HOST,
port: config.DATABASE.PORT,
database: config.DATABASE.DATABASE,
user: config.DATABASE.USERNAME,
password: config.DATABASE.PASSWORD,
connectionLimit: 10,
supportBigNumbers: true,
timezone: '+00:00',
};
private checkDBFlag() {
if (config.DATABASE.ENABLED === false) {
logger.err('Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue');
}
}
public async query(query, params?) {
this.checkDBFlag();
const pool = await this.getPool();
return pool.query(query, params);
}
public async checkDbConnection() {
this.checkDBFlag();
try {
await this.query('SELECT ?', [1]);
logger.info('Database connection established.');
} catch (e) {
logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
process.exit(1);
}
}
private async getPool(): Promise<Pool> {
if (this.pool === null) {
this.pool = createPool(this.poolConfig);
this.pool.on('connection', function (newConnection: PoolConnection) {
newConnection.query(`SET time_zone='+00:00'`);
});
}
return this.pool;
}
});
}
export default new DB();
export async function checkDbConnection() {
try {
const connection = await DB.pool.getConnection();
logger.info('Database connection established.');
connection.release();
} catch (e) {
logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
process.exit(1);
}
}

View File

@@ -5,7 +5,7 @@ import * as WebSocket from 'ws';
import * as cluster from 'cluster';
import axios from 'axios';
import DB from './database';
import { checkDbConnection } from './database';
import config from './config';
import routes from './routes';
import blocks from './api/blocks';
@@ -22,11 +22,6 @@ import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
import elementsParser from './api/liquid/elements-parser';
import databaseMigration from './api/database-migration';
import syncAssets from './sync-assets';
import icons from './api/liquid/icons';
import { Common } from './api/common';
import poolsUpdater from './tasks/pools-updater';
import indexer from './indexer';
class Server {
private wss: WebSocket.Server | undefined;
@@ -67,7 +62,7 @@ class Server {
}
async startServer(worker = false) {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
this.app
.use((req: Request, res: Response, next: NextFunction) => {
@@ -83,22 +78,12 @@ class Server {
this.setUpWebsocketHandling();
await syncAssets.syncAssets$();
diskCache.loadMempoolCache();
if (config.DATABASE.ENABLED) {
await DB.checkDbConnection();
await 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');
}
@@ -108,14 +93,6 @@ class Server {
statistics.startStatistics();
}
if (Common.isLiquid()) {
try {
icons.loadIcons();
} catch (e) {
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
}
}
fiatConversion.startService();
this.setUpHttpApiRoutes();
@@ -149,11 +126,8 @@ class Server {
logger.debug(msg);
}
}
await poolsUpdater.updatePoolsJson();
await blocks.$updateBlocks();
await memPool.$updateMempool();
indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.currentBackendRetryInterval = 5;
} catch (e) {
@@ -175,7 +149,7 @@ class Server {
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
if (config.MEMPOOL.NETWORK === 'liquid' && config.DATABASE.ENABLED) {
blocks.setNewBlockCallback(async () => {
try {
await elementsParser.$parse();
@@ -205,7 +179,7 @@ class Server {
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -213,7 +187,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
@@ -223,7 +197,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
const response = await axios.get('https://mempool.space/api/v1/contributors', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -231,25 +205,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
const response = await axios.get('https://mempool.space/api/v1/contributors/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
@@ -261,35 +217,18 @@ class Server {
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', routes.$getStatisticsByTime.bind(routes, '2h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', routes.$getStatisticsByTime.bind(routes, '24h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', routes.$getStatisticsByTime.bind(routes, '1w'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', routes.$getStatisticsByTime.bind(routes, '1m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', routes.$getStatisticsByTime.bind(routes, '3m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', routes.$getStatisticsByTime.bind(routes, '6m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2h', routes.get2HStatistics)
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/24h', routes.get24HStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1w', routes.get1WHStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1m', routes.get1MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3m', routes.get3MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/6m', routes.get6MStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.get1YStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.get2YStatistics.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.get3YStatistics.bind(routes))
;
}
if (Common.indexingEnabled()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', routes.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight)
;
}
if (config.BISQ.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
@@ -311,11 +250,6 @@ class Server {
;
}
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool)
@@ -326,7 +260,10 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
@@ -339,16 +276,7 @@ class Server {
;
}
if (Common.isLiquid()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/icons', routes.getAllLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/featured', routes.$getAllFeaturedLiquidAssets)
.get(config.MEMPOOL.API_URL_PREFIX + 'asset/:assetId/icon', routes.getLiquidIcon)
.get(config.MEMPOOL.API_URL_PREFIX + 'assets/group/:id', routes.$getAssetGroup)
;
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
if (config.MEMPOOL.NETWORK === 'liquid' && config.DATABASE.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
;

View File

@@ -1,54 +0,0 @@
import { Common } from './api/common';
import blocks from './api/blocks';
import mempool from './api/mempool';
import mining from './api/mining';
import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository';
class Indexer {
runIndexer = true;
indexerRunning = false;
constructor() {
}
public reindex() {
if (Common.indexingEnabled()) {
this.runIndexer = true;
}
}
public async $run() {
if (!Common.indexingEnabled() || this.runIndexer === false ||
this.indexerRunning === true || mempool.hasPriority()
) {
return;
}
this.runIndexer = false;
this.indexerRunning = true;
try {
await blocks.$generateBlockDatabase();
await this.$resetHashratesIndexingState();
await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory();
} catch (e) {
this.reindex();
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e));
}
this.indexerRunning = false;
}
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));
}
}
}
export default new Indexer();

View File

@@ -97,9 +97,6 @@ class Logger {
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
this.syslog(syslogmsg);
}
if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
return;
}
if (priority === 'warning') {
priority = 'warn';
}

View File

@@ -1,27 +1,5 @@
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
export interface PoolTag {
id: number; // mysql row id
name: string;
link: string;
regexes: string; // JSON array
addresses: string; // JSON array
slug: string;
}
export interface PoolInfo {
poolId: number; // mysql row id
name: string;
link: string;
blockCount: number;
slug: string;
}
export interface PoolStats extends PoolInfo {
rank: number;
emptyBlocks: number;
}
export interface MempoolBlock {
blockSize: number;
blockVSize: number;
@@ -33,12 +11,6 @@ export interface MempoolBlock {
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
transactions: TransactionStripped[];
}
export interface MempoolBlockDelta {
added: TransactionStripped[];
removed: string[];
}
interface VinStrippedToScriptsig {
@@ -84,26 +56,12 @@ export interface TransactionStripped {
vsize: number;
value: number;
}
export interface BlockExtension {
totalFees?: number;
export interface BlockExtended extends IEsploraApi.Block {
medianFee?: number;
feeRange?: number[];
reward?: number;
coinbaseTx?: TransactionMinerInfo;
matchRate?: number;
pool?: {
id: number;
name: string;
slug: string;
};
avgFee?: number;
avgFeeRate?: number;
coinbaseRaw?: string;
}
export interface BlockExtended extends IEsploraApi.Block {
extras: BlockExtension;
}
export interface TransactionMinerInfo {
@@ -170,7 +128,10 @@ export interface Statistic {
}
export interface OptimizedStatistic {
id: number;
added: string;
unconfirmed_transactions: number;
tx_per_second: number;
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
@@ -206,21 +167,3 @@ export interface IBackendInfo {
gitCommit: string;
version: string;
}
export interface IDifficultyAdjustment {
progressPercent: number;
difficultyChange: number;
estimatedRetargetDate: number;
remainingBlocks: number;
remainingTime: number;
previousRetarget: number;
nextRetargetHeight: number;
timeAvg: number;
timeOffset: number;
}
export interface RewardStats {
totalReward: number;
totalFee: number;
totalTx: number;
}

View File

@@ -1,657 +0,0 @@
import { BlockExtended } from '../mempool.interfaces';
import DB from '../database';
import logger from '../logger';
import { Common } from '../api/common';
import { prepareBlock } from '../utils/blocks-utils';
import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2';
class BlocksRepository {
/**
* Save indexed block data in the database
*/
public async $saveBlockInDatabase(block: BlockExtended) {
try {
const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate
) VALUE (
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?
)`;
const params: any[] = [
block.height,
block.id,
block.timestamp,
block.size,
block.weight,
block.tx_count,
block.extras.coinbaseRaw,
block.difficulty,
block.extras.pool?.id, // Should always be set to something
block.extras.totalFees,
JSON.stringify(block.extras.feeRange),
block.extras.medianFee,
block.extras.reward,
block.version,
block.bits,
block.nonce,
block.merkle_root,
block.previousblockhash,
block.extras.avgFee,
block.extras.avgFeeRate,
];
await DB.query(query, params);
} catch (e: any) {
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`);
} else {
logger.err('Cannot save indexed block into db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
}
/**
* Get all block height that have not been indexed between [startHeight, endHeight]
*/
public async $getMissingBlocksBetweenHeights(startHeight: number, endHeight: number): Promise<number[]> {
if (startHeight < endHeight) {
return [];
}
try {
const [rows]: any[] = await DB.query(`
SELECT height
FROM blocks
WHERE height <= ? AND height >= ?
ORDER BY height DESC;
`, [startHeight, endHeight]);
const indexedBlockHeights: number[] = [];
rows.forEach((row: any) => { indexedBlockHeights.push(row.height); });
const seekedBlocks: number[] = Array.from(Array(startHeight - endHeight + 1).keys(), n => n + endHeight).reverse();
const missingBlocksHeights = seekedBlocks.filter(x => indexedBlockHeights.indexOf(x) === -1);
return missingBlocksHeights;
} catch (e) {
logger.err('Cannot retrieve blocks list to index. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get empty blocks for one or all pools
*/
public async $countEmptyBlocks(poolId: number | null, interval: string | null = null): Promise<any> {
interval = Common.getSqlInterval(interval);
const params: any[] = [];
let query = `SELECT count(height) as count, pools.id as poolId
FROM blocks
JOIN pools on pools.id = blocks.pool_id
WHERE tx_count = 1`;
if (poolId) {
query += ` AND pool_id = ?`;
params.push(poolId);
}
if (interval) {
query += ` AND blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP by pools.id`;
try {
const [rows] = await DB.query(query, params);
return rows;
} catch (e) {
logger.err('Cannot count empty blocks. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return most recent block height
*/
public async $mostRecentBlockHeight(): Promise<number> {
try {
const [row] = await DB.query('SELECT MAX(height) as maxHeight from blocks');
return row[0]['maxHeight'];
} catch (e) {
logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get blocks count for a period
*/
public async $blockCount(poolId: number | null, interval: string | null = null): Promise<number> {
interval = Common.getSqlInterval(interval);
const params: any[] = [];
let query = `SELECT count(height) as blockCount
FROM blocks`;
if (poolId) {
query += ` WHERE pool_id = ?`;
params.push(poolId);
}
if (interval) {
if (poolId) {
query += ` AND`;
} else {
query += ` WHERE`;
}
query += ` blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
try {
const [rows] = await DB.query(query, params);
return <number>rows[0].blockCount;
} catch (e) {
logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get blocks count between two dates
* @param poolId
* @param from - The oldest timestamp
* @param to - The newest timestamp
* @returns
*/
public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise<number> {
const params: any[] = [];
let query = `SELECT
count(height) as blockCount,
max(height) as lastBlockHeight
FROM blocks`;
if (poolId) {
query += ` WHERE pool_id = ?`;
params.push(poolId);
}
if (poolId) {
query += ` AND`;
} else {
query += ` WHERE`;
}
query += ` blockTimestamp BETWEEN FROM_UNIXTIME('${from}') AND FROM_UNIXTIME('${to}')`;
try {
const [rows] = await DB.query(query, params);
return <number>rows[0];
} catch (e) {
logger.err(`Cannot count blocks for this pool (using timestamps). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get blocks count for a period
*/
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
const params: any[] = [];
let query = `SELECT count(height) as blockCount
FROM blocks
WHERE height <= ${startHeight} AND height >= ${endHeight}`;
try {
const [rows] = await DB.query(query, params);
return <number>rows[0].blockCount;
} catch (e) {
logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the oldest indexed block
*/
public async $oldestBlockTimestamp(): Promise<number> {
const query = `SELECT UNIX_TIMESTAMP(blockTimestamp) as blockTimestamp
FROM blocks
ORDER BY height
LIMIT 1;`;
try {
const [rows]: any[] = await DB.query(query);
if (rows.length <= 0) {
return -1;
}
return <number>rows[0].blockTimestamp;
} catch (e) {
logger.err('Cannot get oldest indexed block timestamp. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get blocks mined by a specific mining pool
*/
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<object[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
}
const params: any[] = [];
let query = ` SELECT
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
WHERE pool_id = ?`;
params.push(pool.id);
if (startHeight !== undefined) {
query += ` AND height < ?`;
params.push(startHeight);
}
query += ` ORDER BY height DESC
LIMIT 10`;
try {
const [rows] = await DB.query(query, params);
const blocks: BlockExtended[] = [];
for (const block of <object[]>rows) {
blocks.push(prepareBlock(block));
}
return blocks;
} catch (e) {
logger.err('Cannot get blocks for this pool. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get one block by height
*/
public async $getBlockByHeight(height: number): Promise<object | null> {
try {
const [rows]: any[] = await DB.query(`SELECT
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
JOIN pools ON blocks.pool_id = pools.id
WHERE height = ${height};
`);
if (rows.length <= 0) {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT *, 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
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = '${hash}';
`;
const [rows]: any[] = await DB.query(query);
if (rows.length <= 0) {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return blocks difficulty
*/
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> {
interval = Common.getSqlInterval(interval);
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
// Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += `
ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
try {
const [rows]: any[] = await DB.query(query);
for (const row of rows) {
delete row['rn'];
}
return rows;
} catch (e) {
logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get general block stats
*/
public async $getBlockStats(blockCount: number): Promise<any> {
try {
// We need to use a subquery
const query = `
SELECT MIN(height) as startBlock, MAX(height) as endBlock, SUM(reward) as totalReward, SUM(fees) as totalFee, SUM(tx_count) as totalTx
FROM
(SELECT height, reward, fees, tx_count FROM blocks
ORDER by height DESC
LIMIT ?) as sub`;
const [rows]: any = await DB.query(query, [blockCount]);
return rows[0];
} catch (e) {
logger.err('Cannot generate reward stats. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/*
* Check if the last 10 blocks chain is valid
*/
public async $validateRecentBlocks(): Promise<boolean> {
try {
const [lastBlocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash FROM blocks ORDER BY height DESC LIMIT 10`);
for (let i = 0; i < lastBlocks.length - 1; ++i) {
if (lastBlocks[i].previous_block_hash !== lastBlocks[i + 1].hash) {
logger.warn(`Chain divergence detected at block ${lastBlocks[i].height}, re-indexing most recent data`);
return false;
}
}
return true;
} catch (e) {
return true; // Don't do anything if there is a db error
}
}
/**
* Check if the chain of block hash is valid and delete data from the stale branch if needed
*/
public async $validateChain(): Promise<boolean> {
try {
const start = new Date().getTime();
const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
let partialMsg = false;
let idx = 1;
while (idx < blocks.length) {
if (blocks[idx].height - 1 !== blocks[idx - 1].height) {
if (partialMsg === false) {
logger.info('Some blocks are not indexed, skipping missing blocks during chain validation');
partialMsg = true;
}
++idx;
continue;
}
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
await this.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
return false;
}
++idx;
}
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
return true;
} catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
return true; // Don't do anything if there is a db error
}
}
/**
* Delete blocks from the database from blockHeight
*/
public async $deleteBlocksFrom(blockHeight: number) {
logger.info(`Delete newer blocks from height ${blockHeight} from the database`);
try {
await DB.query(`DELETE FROM blocks where height >= ${blockHeight}`);
} catch (e) {
logger.err('Cannot delete indexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Get the historical averaged block fees
*/
public async $getHistoricalBlockFees(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(fees) as INT) as avgFees
FROM blocks`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot generate block fees history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the historical averaged block rewards
*/
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(reward) as INT) as avgRewards
FROM blocks`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot generate block rewards history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the historical averaged block fee rate percentiles
*/
public async $getHistoricalBlockFeeRates(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(JSON_EXTRACT(fee_span, '$[0]')) as INT) as avgFee_0,
CAST(AVG(JSON_EXTRACT(fee_span, '$[1]')) as INT) as avgFee_10,
CAST(AVG(JSON_EXTRACT(fee_span, '$[2]')) as INT) as avgFee_25,
CAST(AVG(JSON_EXTRACT(fee_span, '$[3]')) as INT) as avgFee_50,
CAST(AVG(JSON_EXTRACT(fee_span, '$[4]')) as INT) as avgFee_75,
CAST(AVG(JSON_EXTRACT(fee_span, '$[5]')) as INT) as avgFee_90,
CAST(AVG(JSON_EXTRACT(fee_span, '$[6]')) as INT) as avgFee_100
FROM blocks`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot generate block fee rates history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the historical averaged block sizes
*/
public async $getHistoricalBlockSizes(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(size) as INT) as avgSize
FROM blocks`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the historical averaged block weights
*/
public async $getHistoricalBlockWeights(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avgHeight,
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
CAST(AVG(weight) as INT) as avgWeight
FROM blocks`;
if (interval !== null) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`;
const [rows]: any = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new BlocksRepository();

View File

@@ -1,217 +0,0 @@
import { escape } from 'mysql2';
import { Common } from '../api/common';
import DB from '../database';
import logger from '../logger';
import PoolsRepository from './PoolsRepository';
class HashratesRepository {
/**
* Save indexed block data in the database
*/
public async $saveHashrates(hashrates: any) {
if (hashrates.length === 0) {
return;
}
let query = `INSERT INTO
hashrates(hashrate_timestamp, avg_hashrate, pool_id, share, type) VALUES`;
for (const hashrate of hashrates) {
query += ` (FROM_UNIXTIME(${hashrate.hashrateTimestamp}), ${hashrate.avgHashrate}, ${hashrate.poolId}, ${hashrate.share}, "${hashrate.type}"),`;
}
query = query.slice(0, -1);
try {
await DB.query(query);
} catch (e: any) {
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate
FROM hashrates`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'daily'`;
} else {
query += ` WHERE hashrates.type = 'daily'`;
}
query += ` ORDER by hashrate_timestamp`;
try {
const [rows]: any[] = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getWeeklyHashrateTimestamps(): Promise<number[]> {
const query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp
FROM hashrates
WHERE type = 'weekly'
GROUP BY hashrate_timestamp`;
try {
const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp);
} catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Returns the current biggest pool hashrate history
*/
public async $getPoolsWeeklyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
FROM hashrates
JOIN pools on pools.id = pool_id`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'weekly'
AND pool_id IN (${topPoolsId})`;
} else {
query += ` WHERE hashrates.type = 'weekly'
AND pool_id IN (${topPoolsId})`;
}
query += ` ORDER by hashrate_timestamp, FIELD(pool_id, ${topPoolsId})`;
try {
const [rows]: any[] = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Returns a pool hashrate history
*/
public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
const pool = await PoolsRepository.$getPool(slug);
if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug));
}
// Find hashrate boundaries
let query = `SELECT MIN(hashrate_timestamp) as firstTimestamp, MAX(hashrate_timestamp) as lastTimestamp
FROM hashrates
JOIN pools on pools.id = pool_id
WHERE hashrates.type = 'weekly' AND pool_id = ? AND avg_hashrate != 0
ORDER by hashrate_timestamp LIMIT 1`;
let boundaries = {
firstTimestamp: '1970-01-01',
lastTimestamp: '9999-01-01'
};
try {
const [rows]: any[] = await DB.query(query, [pool.id]);
boundaries = rows[0];
} catch (e) {
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e));
}
// Get hashrates entries between boundaries
query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
FROM hashrates
JOIN pools on pools.id = pool_id
WHERE hashrates.type = 'weekly' AND hashrate_timestamp BETWEEN ? AND ?
AND pool_id = ?
ORDER by hashrate_timestamp`;
try {
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
return rows;
} catch (e) {
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;
}
}
/**
* Get latest run timestamp
*/
public async $getLatestRun(key: string): Promise<number> {
const query = `SELECT number FROM state WHERE name = ?`;
try {
const [rows]: any[] = await DB.query(query, [key]);
if (rows.length === 0) {
return 0;
}
return rows[0]['number'];
} catch (e) {
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Delete most recent data points for re-indexing
*/
public async $deleteLastEntries() {
logger.info(`Delete latest hashrates data points from the database`);
try {
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
for (const row of rows) {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp = ?`, [row.timestamp]);
}
// Re-run the hashrate indexing to fill up missing data
await this.$setLatestRun('last_hashrates_indexing', 0);
await this.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Delete hashrates from the database from timestamp
*/
public async $deleteHashratesFromTimestamp(timestamp: number) {
logger.info(`Delete newer hashrates from timestamp ${new Date(timestamp * 1000).toUTCString()} from the database`);
try {
await DB.query(`DELETE FROM hashrates WHERE hashrate_timestamp >= FROM_UNIXTIME(?)`, [timestamp]);
// Re-run the hashrate indexing to fill up missing data
await this.$setLatestRun('last_hashrates_indexing', 0);
await this.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) {
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
export default new HashratesRepository();

View File

@@ -1,99 +0,0 @@
import { Common } from '../api/common';
import config from '../config';
import DB from '../database';
import logger from '../logger';
import { PoolInfo, PoolTag } from '../mempool.interfaces';
class PoolsRepository {
/**
* Get all pools tagging info
*/
public async $getPools(): Promise<PoolTag[]> {
const [rows] = await DB.query('SELECT id, name, addresses, regexes, slug FROM pools;');
return <PoolTag[]>rows;
}
/**
* Get unknown pool tagging info
*/
public async $getUnknownPool(): Promise<PoolTag> {
const [rows] = await DB.query('SELECT id, name, slug FROM pools where name = "Unknown"');
return <PoolTag>rows[0];
}
/**
* Get basic pool info and block count
*/
public async $getPoolsInfo(interval: string | null = null): Promise<PoolInfo[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT COUNT(height) as blockCount, pool_id as poolId, pools.name as name, pools.link as link, slug
FROM blocks
JOIN pools on pools.id = pool_id`;
if (interval) {
query += ` WHERE blocks.blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY pool_id
ORDER BY COUNT(height) DESC`;
try {
const [rows] = await DB.query(query);
return <PoolInfo[]>rows;
} catch (e) {
logger.err(`Cannot generate pools stats. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get basic pool info and block count between two timestamp
*/
public async $getPoolsInfoBetween(from: number, to: number): Promise<PoolInfo[]> {
const query = `SELECT COUNT(height) as blockCount, pools.id as poolId, pools.name as poolName
FROM pools
LEFT JOIN blocks on pools.id = blocks.pool_id AND blocks.blockTimestamp BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
GROUP BY pools.id`;
try {
const [rows] = await DB.query(query, [from, to]);
return <PoolInfo[]>rows;
} catch (e) {
logger.err('Cannot generate pools blocks count. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get mining pool statistics for one pool
*/
public async $getPool(slug: string): Promise<PoolTag | null> {
const query = `
SELECT *
FROM pools
WHERE pools.slug = ?`;
try {
const [rows]: any[] = await DB.query(query, [slug]);
if (rows.length < 1) {
return null;
}
rows[0].regexes = JSON.parse(rows[0].regexes);
if (['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
rows[0].addresses = []; // pools.json only contains mainnet addresses
} else {
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;
}
}
}
export default new PoolsRepository();

View File

@@ -1,21 +0,0 @@
import DB from '../database';
import logger from '../logger';
import { IConversionRates } from '../mempool.interfaces';
class RatesRepository {
public async $saveRate(height: number, rates: IConversionRates) {
try {
await DB.query(`INSERT INTO rates(height, bisq_rates) VALUE (?, ?)`, [height, JSON.stringify(rates)]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Rate already exists for block ${height}, ignoring`);
} else {
logger.err(`Cannot save exchange rate into db for block ${height} Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
}
export default new RatesRepository();

View File

@@ -19,61 +19,45 @@ import loadingIndicators from './api/loading-indicators';
import { Common } from './api/common';
import bitcoinClient from './api/bitcoin/bitcoin-client';
import elementsParser from './api/liquid/elements-parser';
import icons from './api/liquid/icons';
import miningStats from './api/mining';
import axios from 'axios';
import mining from './api/mining';
import BlocksRepository from './repositories/BlocksRepository';
import HashratesRepository from './repositories/HashratesRepository';
import difficultyAdjustment from './api/difficulty-adjustment';
class Routes {
constructor() {}
public async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
public async get2HStatistics(req: Request, res: Response) {
const result = await statistics.$list2H();
res.json(result);
}
try {
let result;
switch (time as string) {
case '2h':
result = await statistics.$list2H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
break;
case '24h':
result = await statistics.$list24H();
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
break;
case '1w':
result = await statistics.$list1W();
break;
case '1m':
result = await statistics.$list1M();
break;
case '3m':
result = await statistics.$list3M();
break;
case '6m':
result = await statistics.$list6M();
break;
case '1y':
result = await statistics.$list1Y();
break;
case '2y':
result = await statistics.$list2Y();
break;
case '3y':
result = await statistics.$list3Y();
break;
default:
result = await statistics.$list2H();
}
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
public get24HStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['24h']);
}
public get1WHStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1w']);
}
public get1MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1m']);
}
public get3MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['3m']);
}
public get6MStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['6m']);
}
public get1YStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['1y']);
}
public get2YStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['2y']);
}
public get3YStatistics(req: Request, res: Response) {
res.json(statistics.getCache()['3y']);
}
public getInitData(req: Request, res: Response) {
@@ -85,7 +69,7 @@ class Routes {
}
}
public getRecommendedFees(req: Request, res: Response) {
public async getRecommendedFees(req: Request, res: Response) {
if (!mempool.isInSync()) {
res.statusCode = 503;
res.send('Service Unavailable');
@@ -537,180 +521,10 @@ class Routes {
}
}
public async $getPool(req: Request, res: Response) {
try {
const stats = await mining.$getPoolStat(req.params.slug);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
public async $getPoolBlocks(req: Request, res: Response) {
try {
const poolBlocks = await BlocksRepository.$getBlocksByPool(
req.params.slug,
req.params.height === undefined ? undefined : parseInt(req.params.height, 10),
);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(poolBlocks);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
public async $getPools(req: Request, res: Response) {
try {
const stats = await miningStats.$getPoolsStats(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getPoolHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolWeeklyHashrate(req.params.slug);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates);
} catch (e) {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
res.status(404).send(e.message);
} else {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
public async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0;
try {
currentHashrate = await bitcoinClient.getNetworkHashPs();
currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
}
try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json({
hashrates: hashrates,
difficulty: difficulty,
currentHashrate: currentHashrate,
currentDifficulty: currentDifficulty,
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getHistoricalBlockFees(req: Request, res: Response) {
try {
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getHistoricalBlockRewards(req: Request, res: Response) {
try {
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getHistoricalBlockFeeRates(req: Request, res: Response) {
try {
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) {
try {
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json({
sizes: blockSizes,
weights: blockWeights
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlock(req: Request, res: Response) {
try {
const block = await blocks.$getBlock(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(block);
const result = await bitcoinApi.$getBlock(req.params.hash);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
@@ -728,20 +542,8 @@ class Routes {
public async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15));
} else { // Liquid, Bisq
return await this.getLegacyBlocks(req, res);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
loadingIndicators.setProgress('blocks', 0);
public async getLegacyBlocks(req: Request, res: Response) {
try {
const returnBlocks: IEsploraApi.Block[] = [];
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
@@ -755,7 +557,7 @@ class Routes {
}
let nextHash = startFromHash;
for (let i = 0; i < 10 && nextHash; i++) {
for (let i = 0; i < 10; i++) {
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
if (localBlock) {
returnBlocks.push(localBlock);
@@ -765,15 +567,16 @@ class Routes {
returnBlocks.push(block);
nextHash = block.previousblockhash;
}
loadingIndicators.setProgress('blocks', i / 10 * 100);
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks);
} catch (e) {
loadingIndicators.setProgress('blocks', 100);
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlockTransactions(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@@ -785,9 +588,9 @@ class Routes {
const endIndex = Math.min(startingIndex + 10, txIds.length);
for (let i = startingIndex; i < endIndex; i++) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true);
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
}
@@ -864,13 +667,7 @@ class Routes {
}
public async getMempool(req: Request, res: Response) {
const info = mempool.getMempoolInfo();
res.json({
count: info.size,
vsize: info.bytes,
total_fee: info.total_fee * 1e8,
fee_histogram: []
});
res.status(501).send('Not implemented');
}
public async getMempoolTxIds(req: Request, res: Response) {
@@ -909,18 +706,61 @@ class Routes {
}
}
public async getTransactionOutspends(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
public getTransactionOutspends(req: Request, res: Response) {
res.status(501).send('Not implemented');
}
public getDifficultyChange(req: Request, res: Response) {
try {
res.json(difficultyAdjustment.getDifficultyAdjustment());
const DATime = blocks.getLastDifficultyAdjustmentTime();
const previousRetarget = blocks.getPreviousDifficultyRetarget();
const blockHeight = blocks.getCurrentBlockHeight();
const now = new Date().getTime() / 1000;
const diff = now - DATime;
const blocksInEpoch = blockHeight % 2016;
const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100;
const remainingBlocks = 2016 - blocksInEpoch;
const nextRetargetHeight = blockHeight + remainingBlocks;
let difficultyChange = 0;
if (remainingBlocks < 1870) {
if (blocksInEpoch > 0) {
difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100;
}
if (difficultyChange > 300) {
difficultyChange = 300;
}
if (difficultyChange < -75) {
difficultyChange = -75;
}
}
const timeAvgDiff = difficultyChange * 0.1;
let timeAvgMins = 10;
if (timeAvgDiff > 0) {
timeAvgMins -= Math.abs(timeAvgDiff);
} else {
timeAvgMins += Math.abs(timeAvgDiff);
}
const timeAvg = timeAvgMins * 60;
const remainingTime = remainingBlocks * timeAvg;
const estimatedRetargetDate = remainingTime + now;
const result = {
progressPercent,
difficultyChange,
estimatedRetargetDate,
remainingBlocks,
remainingTime,
previousRetarget,
nextRetargetHeight,
timeAvg,
};
res.json(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
@@ -967,55 +807,6 @@ class Routes {
: (e.message || 'Error'));
}
}
public getLiquidIcon(req: Request, res: Response) {
const result = icons.getIconByAssetId(req.params.assetId);
if (result) {
res.setHeader('content-type', 'image/png');
res.setHeader('content-length', result.length);
res.send(result);
} else {
res.status(404).send('Asset icon not found');
}
}
public getAllLiquidIcon(req: Request, res: Response) {
const result = icons.getAllIconIds();
if (result) {
res.json(result);
} else {
res.status(404).send('Asset icons not found');
}
}
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
public async $getAssetGroup(req: Request, res: Response) {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`,
{ responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
}
public async $getRewardStats(req: Request, res: Response) {
try {
const response = await mining.$getRewardStats(parseInt(req.params.blockCount, 10));
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response);
} catch (e) {
res.status(500).end();
}
}
}
export default new Routes();

View File

@@ -1,92 +0,0 @@
module.exports = {
addMultiSigAddress: 'addmultisigaddress',
addNode: 'addnode', // bitcoind v0.8.0+
backupWallet: 'backupwallet',
createMultiSig: 'createmultisig',
createRawTransaction: 'createrawtransaction', // bitcoind v0.7.0+
decodeRawTransaction: 'decoderawtransaction', // bitcoind v0.7.0+
decodeScript: 'decodescript',
dumpPrivKey: 'dumpprivkey',
dumpWallet: 'dumpwallet', // bitcoind v0.9.0+
encryptWallet: 'encryptwallet',
estimateFee: 'estimatefee', // bitcoind v0.10.0x
estimatePriority: 'estimatepriority', // bitcoind v0.10.0+
generate: 'generate', // bitcoind v0.11.0+
getAccount: 'getaccount',
getAccountAddress: 'getaccountaddress',
getAddedNodeInfo: 'getaddednodeinfo', // bitcoind v0.8.0+
getAddressesByAccount: 'getaddressesbyaccount',
getBalance: 'getbalance',
getBestBlockHash: 'getbestblockhash', // bitcoind v0.9.0+
getBlock: 'getblock',
getBlockStats: 'getblockstats',
getBlockFilter: 'getblockfilter',
getBlockchainInfo: 'getblockchaininfo', // bitcoind v0.9.2+
getBlockCount: 'getblockcount',
getBlockHash: 'getblockhash',
getBlockHeader: 'getblockheader',
getBlockTemplate: 'getblocktemplate', // bitcoind v0.7.0+
getChainTips: 'getchaintips', // bitcoind v0.10.0+
getChainTxStats: 'getchaintxstats',
getConnectionCount: 'getconnectioncount',
getDifficulty: 'getdifficulty',
getGenerate: 'getgenerate',
getInfo: 'getinfo',
getMempoolAncestors: 'getmempoolancestors',
getMempoolDescendants: 'getmempooldescendants',
getMempoolEntry: 'getmempoolentry',
getMempoolInfo: 'getmempoolinfo', // bitcoind v0.10+
getMiningInfo: 'getmininginfo',
getNetTotals: 'getnettotals',
getNetworkInfo: 'getnetworkinfo', // bitcoind v0.9.2+
getNetworkHashPs: 'getnetworkhashps', // bitcoind v0.9.0+
getNewAddress: 'getnewaddress',
getPeerInfo: 'getpeerinfo', // bitcoind v0.7.0+
getRawChangeAddress: 'getrawchangeaddress', // bitcoin v0.9+
getRawMemPool: 'getrawmempool', // bitcoind v0.7.0+
getRawTransaction: 'getrawtransaction', // bitcoind v0.7.0+
getReceivedByAccount: 'getreceivedbyaccount',
getReceivedByAddress: 'getreceivedbyaddress',
getTransaction: 'gettransaction',
getTxOut: 'gettxout', // bitcoind v0.7.0+
getTxOutProof: 'gettxoutproof', // bitcoind v0.11.0+
getTxOutSetInfo: 'gettxoutsetinfo', // bitcoind v0.7.0+
getUnconfirmedBalance: 'getunconfirmedbalance', // bitcoind v0.9.0+
getWalletInfo: 'getwalletinfo', // bitcoind v0.9.2+
help: 'help',
importAddress: 'importaddress', // bitcoind v0.10.0+
importPrivKey: 'importprivkey',
importWallet: 'importwallet', // bitcoind v0.9.0+
keypoolRefill: 'keypoolrefill',
keyPoolRefill: 'keypoolrefill',
listAccounts: 'listaccounts',
listAddressGroupings: 'listaddressgroupings', // bitcoind v0.7.0+
listLockUnspent: 'listlockunspent', // bitcoind v0.8.0+
listReceivedByAccount: 'listreceivedbyaccount',
listReceivedByAddress: 'listreceivedbyaddress',
listSinceBlock: 'listsinceblock',
listTransactions: 'listtransactions',
listUnspent: 'listunspent', // bitcoind v0.7.0+
lockUnspent: 'lockunspent', // bitcoind v0.8.0+
move: 'move',
ping: 'ping', // bitcoind v0.9.0+
prioritiseTransaction: 'prioritisetransaction', // bitcoind v0.10.0+
sendFrom: 'sendfrom',
sendMany: 'sendmany',
sendRawTransaction: 'sendrawtransaction', // bitcoind v0.7.0+
sendToAddress: 'sendtoaddress',
setAccount: 'setaccount',
setGenerate: 'setgenerate',
setTxFee: 'settxfee',
signMessage: 'signmessage',
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+
validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage',
verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+
walletLock: 'walletlock',
walletPassphrase: 'walletpassphrase',
walletPassphraseChange: 'walletpassphrasechange'
}

View File

@@ -1,61 +0,0 @@
var commands = require('./commands')
var rpc = require('./jsonrpc')
// ===----------------------------------------------------------------------===//
// JsonRPC
// ===----------------------------------------------------------------------===//
function Client (opts) {
// @ts-ignore
this.rpc = new rpc.JsonRPC(opts)
}
// ===----------------------------------------------------------------------===//
// cmd
// ===----------------------------------------------------------------------===//
Client.prototype.cmd = function () {
var args = [].slice.call(arguments)
var cmd = args.shift()
callRpc(cmd, args, this.rpc)
}
// ===----------------------------------------------------------------------===//
// callRpc
// ===----------------------------------------------------------------------===//
function callRpc (cmd, args, rpc) {
var fn = args[args.length - 1]
// If the last argument is a callback, pop it from the args list
if (typeof fn === 'function') {
args.pop()
} else {
fn = function () {}
}
return rpc.call(cmd, args, function () {
var args = [].slice.call(arguments)
// @ts-ignore
args.unshift(null)
// @ts-ignore
fn.apply(this, args)
}, function (err) {
fn(err)
})
}
// ===----------------------------------------------------------------------===//
// Initialize wrappers
// ===----------------------------------------------------------------------===//
(function () {
for (var protoFn in commands) {
(function (protoFn) {
Client.prototype[protoFn] = function () {
var args = [].slice.call(arguments)
return callRpc(commands[protoFn], args, this.rpc)
}
})(protoFn)
}
})()
// Export!
module.exports.Client = Client;

View File

@@ -1,162 +0,0 @@
var http = require('http')
var https = require('https')
var JsonRPC = function (opts) {
// @ts-ignore
this.opts = opts || {}
// @ts-ignore
this.http = this.opts.ssl ? https : http
}
JsonRPC.prototype.call = function (method, params) {
return new Promise((resolve, reject) => {
var time = Date.now()
var requestJSON
if (Array.isArray(method)) {
// multiple rpc batch call
requestJSON = []
method.forEach(function (batchCall, i) {
requestJSON.push({
id: time + '-' + i,
method: batchCall.method,
params: batchCall.params
})
})
} else {
// single rpc call
requestJSON = {
id: time,
method: method,
params: params
}
}
// First we encode the request into JSON
requestJSON = JSON.stringify(requestJSON)
// prepare request options
var requestOptions = {
host: this.opts.host || 'localhost',
port: this.opts.port || 8332,
method: 'POST',
path: '/',
headers: {
'Host': this.opts.host || 'localhost',
'Content-Length': requestJSON.length
},
agent: false,
rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false
}
if (this.opts.ssl && this.opts.sslCa) {
// @ts-ignore
requestOptions.ca = this.opts.sslCa
}
// use HTTP auth if user and password set
if (this.opts.user && this.opts.pass) {
// @ts-ignore
requestOptions.auth = this.opts.user + ':' + this.opts.pass
}
// Now we'll make a request to the server
var cbCalled = false
var request = this.http.request(requestOptions)
// start request timeout timer
var reqTimeout = setTimeout(function () {
if (cbCalled) return
cbCalled = true
request.abort()
var err = new Error('ETIMEDOUT')
// @ts-ignore
err.code = 'ETIMEDOUT'
reject(err)
}, this.opts.timeout || 30000)
// set additional timeout on socket in case of remote freeze after sending headers
request.setTimeout(this.opts.timeout || 30000, function () {
if (cbCalled) return
cbCalled = true
request.abort()
var err = new Error('ESOCKETTIMEDOUT')
// @ts-ignore
err.code = 'ESOCKETTIMEDOUT'
reject(err)
})
request.on('error', function (err) {
if (cbCalled) return
cbCalled = true
clearTimeout(reqTimeout)
reject(err)
})
request.on('response', function (response) {
clearTimeout(reqTimeout)
// We need to buffer the response chunks in a nonblocking way.
var buffer = ''
response.on('data', function (chunk) {
buffer = buffer + chunk
})
// When all the responses are finished, we decode the JSON and
// depending on whether it's got a result or an error, we call
// emitSuccess or emitError on the promise.
response.on('end', function () {
var err
if (cbCalled) return
cbCalled = true
try {
var decoded = JSON.parse(buffer)
} catch (e) {
if (response.statusCode !== 200) {
err = new Error('Invalid params, response status code: ' + response.statusCode)
err.code = -32602
reject(err)
} else {
err = new Error('Problem parsing JSON response from server')
err.code = -32603
reject(err)
}
return
}
if (!Array.isArray(decoded)) {
decoded = [decoded]
}
// iterate over each response, normally there will be just one
// unless a batch rpc call response is being processed
decoded.forEach(function (decodedResponse, i) {
if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) {
if (reject) {
err = new Error(decodedResponse.error.message || '')
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code
}
reject(err)
}
} else if (decodedResponse.hasOwnProperty('result')) {
// @ts-ignore
resolve(decodedResponse.result, response.headers)
} else {
if (reject) {
err = new Error(decodedResponse.error.message || '')
if (decodedResponse.error.code) {
err.code = decodedResponse.error.code
}
reject(err)
}
}
})
})
})
request.end(requestJSON);
});
}
module.exports.JsonRPC = JsonRPC

View File

@@ -1,85 +0,0 @@
import axios, { AxiosResponse } from 'axios';
import * as fs from 'fs';
import config from './config';
import backendInfo from './api/backend-info';
import logger from './logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
const PATH = './';
class SyncAssets {
constructor() { }
public async syncAssets$() {
for (const url of config.MEMPOOL.EXTERNAL_ASSETS) {
try {
await this.downloadFile$(url);
} catch (e) {
throw new Error(`Failed to download external asset. ` + (e instanceof Error ? e.message : e));
}
}
}
private async downloadFile$(url: string) {
return new Promise((resolve, reject) => {
const fileName = url.split('/').slice(-1)[0];
try {
if (config.SOCKS5PROXY.ENABLED) {
const 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;
}
const agent = new SocksProxyAgent(socksOptions);
logger.info(`Downloading external asset ${fileName} over the Tor network...`);
return axios.get(url, {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
httpAgent: agent,
httpsAgent: agent,
responseType: 'stream',
timeout: 30000
}).then(function (response) {
const writer = fs.createWriteStream(PATH + fileName);
writer.on('finish', () => {
logger.info(`External asset ${fileName} saved to ${PATH + fileName}`);
resolve(0);
});
response.data.pipe(writer);
});
} else {
logger.info(`Downloading external asset ${fileName} over clearnet...`);
return axios.get(url, {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
responseType: 'stream',
timeout: 30000
}).then(function (response) {
const writer = fs.createWriteStream(PATH + fileName);
writer.on('finish', () => {
logger.info(`External asset ${fileName} saved to ${PATH + fileName}`);
resolve(0);
});
response.data.pipe(writer);
});
}
} catch (e: any) {
reject(e);
}
});
}
}
export default new SyncAssets();

View File

@@ -1,175 +0,0 @@
import axios, { AxiosResponse } from 'axios';
import poolsParser from '../api/pools-parser';
import config from '../config';
import DB from '../database';
import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
/**
* Maintain the most recent version of pools.json
*/
class PoolsUpdater {
lastRun: number = 0;
currentSha: any = undefined;
constructor() {
}
public async updatePoolsJson() {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return;
}
const oneWeek = 604800;
const oneDay = 86400;
const now = new Date().getTime() / 1000;
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
return;
}
this.lastRun = now;
logger.info('Updating latest mining pools from Github');
if (config.SOCKS5PROXY.ENABLED) {
logger.info('List of public pools will be queried over the Tor network');
} else {
logger.info('List of public pools will be queried over clearnet');
}
try {
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
if (githubSha === undefined) {
return;
}
if (config.DATABASE.ENABLED === true) {
this.currentSha = await this.getShaFromDb();
}
logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== undefined && this.currentSha === githubSha) {
return;
}
logger.warn('Pools.json is outdated, fetch latest from github');
const poolsJson = await this.query('https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json');
if (poolsJson === undefined) {
return;
}
await poolsParser.migratePoolsJson(poolsJson);
await this.updateDBSha(githubSha);
logger.notice('PoolsUpdater completed');
} catch (e) {
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
}
}
/**
* Fetch our latest pools.json sha from the db
*/
private async updateDBSha(githubSha: string) {
this.currentSha = githubSha;
if (config.DATABASE.ENABLED === true) {
try {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
/**
* Fetch our latest pools.json sha from the db
*/
private async getShaFromDb(): Promise<string | undefined> {
try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : undefined);
} catch (e) {
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
return undefined;
}
}
/**
* Fetch our latest pools.json sha from github
*/
private async fetchPoolsSha(): Promise<string | undefined> {
const response = await this.query('https://api.github.com/repos/mempool/mining-pools/git/trees/master');
if (response !== undefined) {
for (const file of response['tree']) {
if (file['path'] === 'pools.json') {
return file['sha'];
}
}
}
logger.err('Cannot to find latest pools.json sha from github api response');
return undefined;
}
/**
* Http request wrapper
*/
private async query(path): Promise<object | undefined> {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
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) {
const 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}`;
}
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
const data: AxiosResponse = await axios.get(path, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
}
return undefined;
}
}
export default new PoolsUpdater();

View File

@@ -1,32 +0,0 @@
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.blockTimestamp, // blockTimestamp for indexed block
height: block.height,
version: block.version,
bits: block.bits,
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkle_root,
tx_count: block.tx_count,
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),
}
};
}

View File

@@ -10,8 +10,7 @@
"moduleResolution": "node",
"typeRoots": [
"node_modules/@types"
],
"allowSyntheticDefaultImports": true
]
},
"include": [
"src/**/*.ts"

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

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

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 May 15, 2022.
Signed: ayanamidev

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: Bosch-0

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

View File

@@ -1 +0,0 @@
Mempool Space K.K. has a signed CLA or other agreement on file with @emzy as of January 25, 2022

View File

@@ -1 +0,0 @@
Mempool Space K.K. has a signed CLA or other agreement on file with @hunicus as of January 25, 2022

View File

@@ -1 +0,0 @@
Mempool Space K.K. has a signed CLA or other agreement on file with @knorrium as of January 25, 2022

View File

@@ -1 +0,0 @@
Mempool Space K.K. has a signed CLA or other agreement on file with @miguelmedeiros as of January 25, 2022

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 May 31, 2022.
Signed: mononaut

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 March 11, 2022.
Signed: naveensrinivasan

View File

@@ -1 +0,0 @@
Mempool Space K.K. has a signed CLA or other agreement on file with @nymkappa as of January 25, 2022

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

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

View File

@@ -1,346 +1,101 @@
# Docker Installation
# Docker
This directory contains the Dockerfiles used to build and release the official images, as well as a `docker-compose.yml` to configure environment variables and other settings.
## Initialization
If you are looking to use these Docker images to deploy your own instance of Mempool, note that they only containerize Mempool's frontend and backend. You will still need to deploy and configure Bitcoin Core and an Electrum Server separately, along with any other utilities specific to your use case (e.g., a reverse proxy, etc). Such configuration is mostly beyond the scope of the Mempool project, so please only proceed if you know what you're doing.
Jump to a section in this doc:
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)
- [Further Configuration](#further-configuration)
## Configure with Bitcoin Core Only
_Note: address lookups require an Electrum Server and will not work with this configuration. [Add an Electrum Server](#configure-with-bitcoin-core--electrum-server) to your backend for full functionality._
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
```
txindex=1
server=1
rpcuser=mempool
rpcpassword=mempool
```
If you want to use different credentials, specify them in the `docker-compose.yml` file:
```
api:
environment:
MEMPOOL_BACKEND: "none"
CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "customuser"
CORE_RPC_PASSWORD: "custompassword"
```
The IP address in the example above refers to Docker's default gateway IP address so that the container can hit the `bitcoind` instance running on the host machine. If your setup is different, update it accordingly.
Make sure `bitcoind` is running and synced.
Now, run:
In an empty dir create 2 sub-dirs
```bash
docker-compose up
mkdir -p data mysql/data mysql/db-scripts
```
Your Mempool instance should be running at http://localhost. The graphs will be populated as new transactions are detected.
In the `mysql/db-scripts` sub-dir add the `mariadb-structure.sql` file from the mempool repo
## Configure with Bitcoin Core + Electrum Server
Your dir should now look like that:
First, configure `bitcoind` as specified above, and make sure your Electrum Server is running and synced. See [this FAQ](https://mempool.space/docs/faq#address-lookup-issues) if you need help picking an Electrum Server implementation.
```bash
$ls -R
.:
data mysql
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
./data:
./mysql:
data db-scripts
./mysql/data:
./mysql/db-scripts:
mariadb-structure.sql
```
api:
In the main dir add the following `docker-compose.yml`
```bash
version: "3.7"
services:
web:
image: mempool/frontend:latest
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 80:8080
environment:
MEMPOOL_BACKEND: "electrum"
ELECTRUM_HOST: "172.27.0.1"
FRONTEND_HTTP_PORT: "8080"
BACKEND_MAINNET_HTTP_HOST: "api"
api:
image: mempool/backend:latest
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
command: "./wait-for-it.sh db:3306 --timeout=720 --strict -- ./start.sh"
volumes:
- ./data:/backend/cache
environment:
RPC_HOST: "127.0.0.1"
RPC_PORT: "8332"
RPC_USER: "mempool"
RPC_PASS: "mempool"
ELECTRUM_HOST: "127.0.0.1"
ELECTRUM_PORT: "50002"
ELECTRUM_TLS_ENABLED: "false"
ELECTRUM_TLS: "false"
MYSQL_HOST: "db"
MYSQL_PORT: "3306"
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASS: "mempool"
BACKEND_MAINNET_HTTP_PORT: "8999"
CACHE_DIR: "/backend/cache"
MEMPOOL_CLEAR_PROTECTION_MINUTES: "20"
db:
image: mariadb:10.5.8
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/db-scripts:/docker-entrypoint-initdb.d
environment:
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin"
```
Eligible values for `MEMPOOL_BACKEND`:
- "electrum" if you're using [romanz/electrs](https://github.com/romanz/electrs) or [cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)
- "esplora" if you're using [Blockstream/electrs](https://github.com/Blockstream/electrs)
- "none" if you're not using any Electrum Server
You can update all the environment variables inside the API container, especially the RPC and ELECTRUM ones
Of course, if your Docker host IP address is different, update accordingly.
## Run it
With `bitcoind` and Electrum Server set up, run Mempool with:
To run our docker-compose use the following cmd:
```bash
docker-compose up
```
## Further Configuration
If everything went okay you should see the beautiful mempool :grin:
Optionally, you can override any other backend settings from `mempool-config.json`.
Below we list all settings from `mempool-config.json` and the corresponding overrides you can make in the `api` > `environment` section of `docker-compose.yml`.
<br/>
`mempool-config.json`:
```
"MEMPOOL": {
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": 8999,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "./cache",
"CLEAR_PROTECTION_MINUTES": 20,
"RECOMMENDED_FEE_PERCENTILE": 50,
"BLOCK_WEIGHT_UNITS": 4000000,
"INITIAL_BLOCKS_AMOUNT": 8,
"MEMPOOL_BLOCKS_AMOUNT": 8,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"],
"STDOUT_LOG_MIN_PRIORITY": "info"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
MEMPOOL_NETWORK: ""
MEMPOOL_BACKEND: ""
MEMPOOL_HTTP_PORT: ""
MEMPOOL_SPAWN_CLUSTER_PROCS: ""
MEMPOOL_API_URL_PREFIX: ""
MEMPOOL_POLL_RATE_MS: ""
MEMPOOL_CACHE_DIR: ""
MEMPOOL_CLEAR_PROTECTION_MINUTES: ""
MEMPOOL_RECOMMENDED_FEE_PERCENTILE: ""
MEMPOOL_BLOCK_WEIGHT_UNITS: ""
MEMPOOL_INITIAL_BLOCKS_AMOUNT: ""
MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: ""
MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: ""
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
MEMPOOL_EXTERNAL_ASSETS: ""
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
...
```
<br/>
`mempool-config.json`:
```
"CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
CORE_RPC_HOST: ""
CORE_RPC_PORT: ""
CORE_RPC_USERNAME: ""
CORE_RPC_PASSWORD: ""
...
```
<br/>
`mempool-config.json`:
```
"ELECTRUM": {
"HOST": "127.0.0.1",
"PORT": 50002,
"TLS_ENABLED": true
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
ELECTRUM_HOST: ""
ELECTRUM_PORT: ""
ELECTRUM_TLS_ENABLED: ""
...
```
<br/>
`mempool-config.json`:
```
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
ESPLORA_REST_API_URL: ""
...
```
<br/>
`mempool-config.json`:
```
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",
"PORT": 8332,
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
SECOND_CORE_RPC_HOST: ""
SECOND_CORE_RPC_PORT: ""
SECOND_CORE_RPC_USERNAME: ""
SECOND_CORE_RPC_PASSWORD: ""
...
```
<br/>
`mempool-config.json`:
```
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 3306,
"DATABASE": "mempool",
"USERNAME": "mempool",
"PASSWORD": "mempool"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
DATABASE_ENABLED: ""
DATABASE_HOST: ""
DATABASE_PORT: ""
DATABASE_DATABASE: ""
DATABASE_USERAME: ""
DATABASE_PASSWORD: ""
...
```
<br/>
`mempool-config.json`:
```
"SYSLOG": {
"ENABLED": true,
"HOST": "127.0.0.1",
"PORT": 514,
"MIN_PRIORITY": "info",
"FACILITY": "local7"
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
SYSLOG_ENABLED: ""
SYSLOG_HOST: ""
SYSLOG_PORT: ""
SYSLOG_MIN_PRIORITY: ""
SYSLOG_FACILITY: ""
...
```
<br/>
`mempool-config.json`:
```
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
STATISTICS_ENABLED: ""
STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD: ""
...
```
<br/>
`mempool-config.json`:
```
"BISQ": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
BISQ_ENABLED: ""
BISQ_DATA_PATH: ""
...
```
<br/>
`mempool-config.json`:
```
"SOCKS5PROXY": {
"ENABLED": false,
"HOST": "127.0.0.1",
"PORT": "9050",
"USERNAME": "",
"PASSWORD": ""
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
SOCKS5PROXY_ENABLED: ""
SOCKS5PROXY_HOST: ""
SOCKS5PROXY_PORT: ""
SOCKS5PROXY_USERNAME: ""
SOCKS5PROXY_PASSWORD: ""
...
```
<br/>
`mempool-config.json`:
```
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
PRICE_DATA_SERVER_TOR_URL: ""
PRICE_DATA_SERVER_CLEARNET_URL: ""
...
```
If you get stuck on "loading blocks", this means the websocket can't connect.
Check your nginx proxy setup, firewalls, etc. and open an issue if you need help.

View File

@@ -1,7 +1,4 @@
FROM node:16.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
FROM node:16.10.0-buster-slim AS builder
WORKDIR /build
COPY . .
@@ -11,7 +8,7 @@ RUN apt-get install -y build-essential python3 pkg-config
RUN npm install
RUN npm run build
FROM node:16.15.0-buster-slim
FROM node:16.10.0-buster-slim
WORKDIR /backend

View File

@@ -1,88 +1,38 @@
{
"MEMPOOL": {
"NETWORK": "__MEMPOOL_NETWORK__",
"BACKEND": "__MEMPOOL_BACKEND__",
"HTTP_PORT": __MEMPOOL_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": __MEMPOOL_SPAWN_CLUSTER_PROCS__,
"API_URL_PREFIX": "__MEMPOOL_API_URL_PREFIX__",
"POLL_RATE_MS": __MEMPOOL_POLL_RATE_MS__,
"CACHE_DIR": "__MEMPOOL_CACHE_DIR__",
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_CLEAR_PROTECTION_MINUTES__,
"RECOMMENDED_FEE_PERCENTILE": __MEMPOOL_RECOMMENDED_FEE_PERCENTILE__,
"BLOCK_WEIGHT_UNITS": __MEMPOOL_BLOCK_WEIGHT_UNITS__,
"INITIAL_BLOCKS_AMOUNT": __MEMPOOL_INITIAL_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__,
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
"EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__,
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
"NETWORK": "mainnet",
"BACKEND": "electrum",
"HTTP_PORT": __MEMPOOL_BACKEND_MAINNET_HTTP_PORT__,
"SPAWN_CLUSTER_PROCS": 0,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000,
"CACHE_DIR": "__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__",
"CLEAR_PROTECTION_MINUTES": __MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",
"PORT": __CORE_RPC_PORT__,
"USERNAME": "__CORE_RPC_USERNAME__",
"PASSWORD": "__CORE_RPC_PASSWORD__"
"HOST": "__BITCOIN_MAINNET_RPC_HOST__",
"PORT": __BITCOIN_MAINNET_RPC_PORT__,
"USERNAME": "__BITCOIN_MAINNET_RPC_USER__",
"PASSWORD": "__BITCOIN_MAINNET_RPC_PASS__"
},
"ELECTRUM": {
"HOST": "__ELECTRUM_HOST__",
"PORT": __ELECTRUM_PORT__,
"TLS_ENABLED": __ELECTRUM_TLS_ENABLED__
"HOST": "__ELECTRUM_MAINNET_HTTP_HOST__",
"PORT": __ELECTRUM_MAINNET_HTTP_PORT__,
"TLS_ENABLED": __ELECTRUM_MAINNET_TLS_ENABLED__
},
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__"
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",
"PORT": __SECOND_CORE_RPC_PORT__,
"USERNAME": "__SECOND_CORE_RPC_USERNAME__",
"PASSWORD": "__SECOND_CORE_RPC_PASSWORD__"
"REST_API_URL": "http://127.0.0.1:3000"
},
"DATABASE": {
"ENABLED": __DATABASE_ENABLED__,
"HOST": "__DATABASE_HOST__",
"SOCKET": "__DATABASE_SOCKET__",
"PORT": __DATABASE_PORT__,
"DATABASE": "__DATABASE_DATABASE__",
"USERNAME": "__DATABASE_USERNAME__",
"PASSWORD": "__DATABASE_PASSWORD__"
},
"SYSLOG": {
"ENABLED": __SYSLOG_ENABLED__,
"HOST": "__SYSLOG_HOST__",
"PORT": __SYSLOG_PORT__,
"MIN_PRIORITY": "__SYSLOG_MIN_PRIORITY__",
"FACILITY": "__SYSLOG_FACILITY__"
"ENABLED": true,
"HOST": "__MYSQL_HOST__",
"PORT": __MYSQL_PORT__,
"DATABASE": "__MYSQL_DATABASE__",
"USERNAME": "__MYSQL_USERNAME__",
"PASSWORD": "__MYSQL_PASSWORD__"
},
"STATISTICS": {
"ENABLED": __STATISTICS_ENABLED__,
"TX_PER_SECOND_SAMPLE_PERIOD": __STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__
},
"BISQ": {
"ENABLED": __BISQ_ENABLED__,
"DATA_PATH": "__BISQ_DATA_PATH__"
},
"SOCKS5PROXY": {
"ENABLED": __SOCKS5PROXY_ENABLED__,
"USE_ONION": __SOCKS5PROXY_USE_ONION__,
"HOST": "__SOCKS5PROXY_HOST__",
"PORT": "__SOCKS5PROXY_PORT__",
"USERNAME": "__SOCKS5PROXY_USERNAME__",
"PASSWORD": "__SOCKS5PROXY_PASSWORD__"
},
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
}
}

View File

@@ -1,155 +1,41 @@
#!/bin/sh
# MEMPOOL
__MEMPOOL_NETWORK__=${MEMPOOL_NETWORK:=mainnet}
__MEMPOOL_BACKEND__=${MEMPOOL_BACKEND:=electrum}
__MEMPOOL_HTTP_PORT__=${BACKEND_HTTP_PORT:=8999}
__MEMPOOL_SPAWN_CLUSTER_PROCS__=${MEMPOOL_SPAWN_CLUSTER_PROCS:=0}
__MEMPOOL_API_URL_PREFIX__=${MEMPOOL_API_URL_PREFIX:=/api/v1/}
__MEMPOOL_POLL_RATE_MS__=${MEMPOOL_POLL_RATE_MS:=2000}
__MEMPOOL_CACHE_DIR__=${MEMPOOL_CACHE_DIR:=./cache}
__MEMPOOL_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__=${MEMPOOL_RECOMMENDED_FEE_PERCENTILE:=50}
__MEMPOOL_BLOCK_WEIGHT_UNITS__=${MEMPOOL_BLOCK_WEIGHT_UNITS:=4000000}
__MEMPOOL_INITIAL_BLOCKS_AMOUNT__=${MEMPOOL_INITIAL_BLOCKS_AMOUNT:=8}
__MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
__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_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
__CORE_RPC_PORT__=${CORE_RPC_PORT:=8332}
__CORE_RPC_USERNAME__=${CORE_RPC_USERNAME:=mempool}
__CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
#MEMPOOL
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__=${CACHE_DIR:=./cache}
__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__=${MEMPOOL_CLEAR_PROTECTION_MINUTES:=20}
# BITCOIN
__BITCOIN_MAINNET_RPC_HOST__=${RPC_HOST:=127.0.0.1}
__BITCOIN_MAINNET_RPC_PORT__=${RPC_PORT:=8332}
__BITCOIN_MAINNET_RPC_USER__=${RPC_USER:=mempool}
__BITCOIN_MAINNET_RPC_PASS__=${RPC_PASS:=mempool}
# ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
__ELECTRUM_PORT__=${ELECTRUM_PORT:=50002}
__ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
__ELECTRUM_MAINNET_HTTP_HOST__=${ELECTRUM_HOST:=127.0.0.1}
__ELECTRUM_MAINNET_HTTP_PORT__=${ELECTRUM_PORT:=50002} # 50001?
__ELECTRUM_MAINNET_TLS_ENABLED__=${ELECTRUM_TLS:=false}
# MYSQL
__MYSQL_HOST__=${MYSQL_HOST:=127.0.0.1}
__MYSQL_PORT__=${MYSQL_PORT:=3306}
__MYSQL_DATABASE__=${MYSQL_DATABASE:=mempool}
__MYSQL_USERNAME__=${MYSQL_USER:=mempool}
__MYSQL_PASSWORD__=${MYSQL_PASS:=mempool}
# ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
mkdir -p "${__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__}"
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
__SECOND_CORE_RPC_PORT__=${SECOND_CORE_RPC_PORT:=8332}
__SECOND_CORE_RPC_USERNAME__=${SECOND_CORE_RPC_USERNAME:=mempool}
__SECOND_CORE_RPC_PASSWORD__=${SECOND_CORE_RPC_PASSWORD:=mempool}
# DATABASE
__DATABASE_ENABLED__=${DATABASE_ENABLED:=true}
__DATABASE_HOST__=${DATABASE_HOST:=127.0.0.1}
__DATABASE_SOCKET__=${DATABASE_SOCKET:=""}
__DATABASE_PORT__=${DATABASE_PORT:=3306}
__DATABASE_DATABASE__=${DATABASE_DATABASE:=mempool}
__DATABASE_USERNAME__=${DATABASE_USERNAME:=mempool}
__DATABASE_PASSWORD__=${DATABASE_PASSWORD:=mempool}
# SYSLOG
__SYSLOG_ENABLED__=${SYSLOG_ENABLED:=false}
__SYSLOG_HOST__=${SYSLOG_HOST:=127.0.0.1}
__SYSLOG_PORT__=${SYSLOG_PORT:=514}
__SYSLOG_MIN_PRIORITY__=${SYSLOG_MIN_PRIORITY:=info}
__SYSLOG_FACILITY__=${SYSLOG_FACILITY:=local7}
# STATISTICS
__STATISTICS_ENABLED__=${STATISTICS_ENABLED:=true}
__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__=${STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD:=150}
# BISQ
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
# SOCKS5PROXY
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
__SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
# PRICE_DATA_SERVER
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
# EXTERNAL_DATA_SERVER
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
sed -i "s/__MEMPOOL_BACKEND__/${__MEMPOOL_BACKEND__}/g" mempool-config.json
sed -i "s/__MEMPOOL_HTTP_PORT__/${__MEMPOOL_HTTP_PORT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_SPAWN_CLUSTER_PROCS__/${__MEMPOOL_SPAWN_CLUSTER_PROCS__}/g" mempool-config.json
sed -i "s!__MEMPOOL_API_URL_PREFIX__!${__MEMPOOL_API_URL_PREFIX__}!g" mempool-config.json
sed -i "s/__MEMPOOL_POLL_RATE_MS__/${__MEMPOOL_POLL_RATE_MS__}/g" mempool-config.json
sed -i "s!__MEMPOOL_CACHE_DIR__!${__MEMPOOL_CACHE_DIR__}!g" mempool-config.json
sed -i "s/__MEMPOOL_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_CLEAR_PROTECTION_MINUTES__}/g" mempool-config.json
sed -i "s/__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__/${__MEMPOOL_RECOMMENDED_FEE_PERCENTILE__}/g" mempool-config.json
sed -i "s/__MEMPOOL_BLOCK_WEIGHT_UNITS__/${__MEMPOOL_BLOCK_WEIGHT_UNITS__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INITIAL_BLOCKS_AMOUNT__/${__MEMPOOL_INITIAL_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_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_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/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_USERNAME__/${__CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PASSWORD__/${__CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__ELECTRUM_HOST__/${__ELECTRUM_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRUM_PORT__/${__ELECTRUM_PORT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config.json
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_USERNAME__/${__SECOND_CORE_RPC_USERNAME__}/g" mempool-config.json
sed -i "s/__SECOND_CORE_RPC_PASSWORD__/${__SECOND_CORE_RPC_PASSWORD__}/g" mempool-config.json
sed -i "s/__DATABASE_ENABLED__/${__DATABASE_ENABLED__}/g" mempool-config.json
sed -i "s/__DATABASE_HOST__/${__DATABASE_HOST__}/g" mempool-config.json
sed -i "s!__DATABASE_SOCKET__!${__DATABASE_SOCKET__}!g" mempool-config.json
sed -i "s/__DATABASE_PORT__/${__DATABASE_PORT__}/g" mempool-config.json
sed -i "s/__DATABASE_DATABASE__/${__DATABASE_DATABASE__}/g" mempool-config.json
sed -i "s/__DATABASE_USERNAME__/${__DATABASE_USERNAME__}/g" mempool-config.json
sed -i "s/__DATABASE_PASSWORD__/${__DATABASE_PASSWORD__}/g" mempool-config.json
sed -i "s/__SYSLOG_ENABLED__/${__SYSLOG_ENABLED__}/g" mempool-config.json
sed -i "s/__SYSLOG_HOST__/${__SYSLOG_HOST__}/g" mempool-config.json
sed -i "s/__SYSLOG_PORT__/${__SYSLOG_PORT__}/g" mempool-config.json
sed -i "s/__SYSLOG_MIN_PRIORITY__/${__SYSLOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__SYSLOG_FACILITY__/${__SYSLOG_FACILITY__}/g" mempool-config.json
sed -i "s/__STATISTICS_ENABLED__/${__STATISTICS_ENABLED__}/g" mempool-config.json
sed -i "s/__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__/${__STATISTICS_TX_PER_SECOND_SAMPLE_PERIOD__}/g" mempool-config.json
sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_HOST__/${__BITCOIN_MAINNET_RPC_HOST__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_PORT__/${__BITCOIN_MAINNET_RPC_PORT__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_USER__/${__BITCOIN_MAINNET_RPC_USER__}/g" mempool-config.json
sed -i "s/__BITCOIN_MAINNET_RPC_PASS__/${__BITCOIN_MAINNET_RPC_PASS__}/g" mempool-config.json
sed -i "s/__ELECTRUM_MAINNET_HTTP_HOST__/${__ELECTRUM_MAINNET_HTTP_HOST__}/g" mempool-config.json
sed -i "s/__ELECTRUM_MAINNET_HTTP_PORT__/${__ELECTRUM_MAINNET_HTTP_PORT__}/g" mempool-config.json
sed -i "s/__ELECTRUM_MAINNET_TLS_ENABLED__/${__ELECTRUM_MAINNET_TLS_ENABLED__}/g" mempool-config.json
sed -i "s/__MYSQL_HOST__/${__MYSQL_HOST__}/g" mempool-config.json
sed -i "s/__MYSQL_PORT__/${__MYSQL_PORT__}/g" mempool-config.json
sed -i "s/__MYSQL_DATABASE__/${__MYSQL_DATABASE__}/g" mempool-config.json
sed -i "s/__MYSQL_USERNAME__/${__MYSQL_USERNAME__}/g" mempool-config.json
sed -i "s/__MYSQL_PASSWORD__/${__MYSQL_PASSWORD__}/g" mempool-config.json
sed -i "s!__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__!${__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__}!g" mempool-config.json
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__/${__MEMPOOL_BACKEND_CLEAR_PROTECTION_MINUTES__}/g" mempool-config.json
node /backend/dist/index.js

View File

View File

@@ -1,10 +1,23 @@
version: "3.7"
services:
web:
electrum:
build:
context: .
dockerfile: docker/electrum/Dockerfile
user: "1000:1000"
restart: on-failure
command: ""
ports:
- 50001:50001
- 50002:50002
- 4224:4224
- 8332:8332
environment:
FRONTEND_HTTP_PORT: "8080"
BACKEND_MAINNET_HTTP_HOST: "api"
ELECTRUM: "electrum"
# add electrs configs
web:
image: mempool/frontend:latest
user: "1000:1000"
restart: on-failure
@@ -12,19 +25,10 @@ services:
command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 80:8080
api:
environment:
MEMPOOL_BACKEND: "none"
CORE_RPC_HOST: "172.27.0.1"
CORE_RPC_PORT: "8332"
CORE_RPC_USERNAME: "mempool"
CORE_RPC_PASSWORD: "mempool"
DATABASE_ENABLED: "true"
DATABASE_HOST: "db"
DATABASE_DATABASE: "mempool"
DATABASE_USERNAME: "mempool"
DATABASE_PASSWORD: "mempool"
STATISTICS_ENABLED: "true"
FRONTEND_HTTP_PORT: "8080"
BACKEND_MAINNET_HTTP_HOST: "api"
api:
image: mempool/backend:latest
user: "1000:1000"
restart: on-failure
@@ -32,15 +36,32 @@ services:
command: "./wait-for-it.sh db:3306 --timeout=720 --strict -- ./start.sh"
volumes:
- ./data:/backend/cache
db:
environment:
RPC_HOST: "127.0.0.1"
RPC_PORT: "8332"
RPC_USER: "mempool"
RPC_PASS: "mempool"
ELECTRUM_HOST: "127.0.0.1"
ELECTRUM_PORT: "50002"
ELECTRUM_TLS: "false"
MYSQL_HOST: "db"
MYSQL_PORT: "3306"
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin"
MYSQL_PASS: "mempool"
BACKEND_MAINNET_HTTP_PORT: "8999"
CACHE_DIR: "/backend/cache"
MEMPOOL_CLEAR_PROTECTION_MINUTES: "20"
db:
image: mariadb:10.5.8
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/db-scripts:/docker-entrypoint-initdb.d
environment:
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin"

View File

@@ -1,4 +1,4 @@
FROM node:16.15.0-buster-slim AS builder
FROM node:16.10.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}

View File

@@ -2,7 +2,7 @@
#backend
gitMaster="\.\.\/\.git\/refs\/heads\/master"
git ls-remote https://github.com/mempool/mempool.git "$1^{}" | awk '{ print $1}' > ./backend/master
git ls-remote https://github.com/mempool/mempool.git $1 | awk '{ print $1}' > ./backend/master
cp ./docker/backend/* ./backend/
sed -i "s/${gitMaster}/master/g" ./backend/src/api/backend-info.ts

3
frontend/.gitignore vendored
View File

@@ -50,10 +50,7 @@ Thumbs.db
src/resources/assets.json
src/resources/assets.minimal.json
src/resources/assets-testnet.json
src/resources/assets-testnet.minimal.json
src/resources/pools.json
src/resources/mining-pools/*
# environment config
mempool-frontend-config.json

View File

@@ -1,30 +1,8 @@
# Mempool Frontend
# mempool-frontend
You can build and run the Mempool frontend and proxy to the production Mempool backend (for easier frontend development), or you can connect it to your own backend for a full Mempool development instance, custom deployment, etc.
## Contributing
Jump to a section in this doc:
- [Quick Setup for Frontend Development](#quick-setup-for-frontend-development)
- [Manual Frontend Setup](#manual-setup)
- [Translations](#translations-transifex-project)
## Quick Setup for Frontend Development
If you want to quickly improve the UI, fix typos, or make other updates that don't require any backend changes, you don't need to set up an entire backend—you can simply run the Mempool frontend locally and proxy to the mempool.space backend.
### 1. Clone Mempool Repository
Get the latest Mempool code:
```
git clone https://github.com/mempool/mempool
cd mempool
```
### 2. Specify Website
The same frontend codebase is used for https://mempool.space, https://liquid.network and https://bisq.markets.
Configure the frontend for the site you want by running the corresponding command:
This package is used for the https://mempool.space, https://liquid.network and https://bisq.markets websites - there are npm scripts to setup all three, which effectively change how BASE_MODULE is configured:
```
$ npm run config:defaults:mempool
@@ -32,22 +10,18 @@ $ npm run config:defaults:liquid
$ npm run config:defaults:bisq
```
### 3. Run the Frontend
Changes that affect the frontend codebase only can be done using the production backend so you don't need to spin up the entire Mempool infrastructure. This is very convenient in case you want to quickly improve the UI, fix typos or implement new features that don't require any backend changes.
_Make sure to use Node.js 16.15 and npm 7._
Install project dependencies and run the frontend server:
Make your changes, install the project dependencies and run the frontend server as follows:
```
$ npm install
$ npm run serve:local-prod
```
The frontend will be available at http://localhost:4200/ and all API requests will be proxied to the production server at https://mempool.space.
The frontend will be available at http://localhost:4200/ and all API requests will be proxied to the production server at https://mempool.space
### 4. Test
After making your changes, you can run our end-to-end automation suite and check for possible regressions.
After making your changes, you can run our end-to-end automation suite and check for possible regressions:
Headless:
@@ -63,43 +37,11 @@ $ npm run config:defaults:mempool && npm run cypress:open
This will open the Cypress test runner, where you can select any of the test files to run.
If all tests are green, submit your PR, and it will be reviewed by someone on the team as soon as possible.
## Manual Setup
Set up the [Mempool backend](../backend/) first, if you haven't already.
### 1. Build the Frontend
_Node.js 16 and npm 7 are recommended._
Build the frontend:
```
cd frontend
npm install # add --prod for production
npm run build
```
### 2. Run the Frontend
#### Development
To run your local Mempool frontend with your local Mempool backend:
```
npm run serve
```
#### Production
The `npm run build` command from step 1 above should have generated a `dist` directory. Put the contents of `dist/` onto your web server.
You will probably want to set up a reverse proxy, TLS, etc. There are sample nginx configuration files in the top level of the repository for reference, but note that support for such tasks is outside the scope of this project.
If all tests are green, submit your PR and it will be reviewed by someone on the team as soon as possible.
## Translations: Transifex Project
The Mempool frontend strings are localized into 20+ locales:
The mempool frontend strings are localized into 20+ locales:
https://www.transifex.com/mempool/mempool/dashboard/
### Translators

View File

@@ -218,10 +218,6 @@
"proxyConfig": "proxy.conf.local.js",
"verbose": true
},
"mixed": {
"proxyConfig": "proxy.conf.mixed.js",
"verbose": true
},
"staging": {
"proxyConfig": "proxy.conf.js",
"disableHostCheck": true,
@@ -233,12 +229,6 @@
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
},
"local-staging": {
"proxyConfig": "proxy.conf.staging.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
}
}
},

View File

@@ -1,23 +0,0 @@
import { defineConfig } from 'cypress'
export default defineConfig({
projectId: 'ry4br7',
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
fixturesFolder: 'cypress/fixtures',
video: false,
retries: {
runMode: 3,
openMode: 0,
},
chromeWebSecurity: false,
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:4200',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
})

16
frontend/cypress.json Normal file
View File

@@ -0,0 +1,16 @@
{
"projectId": "ry4br7",
"integrationFolder": "cypress/integration",
"supportFile": "cypress/support/index.ts",
"videosFolder": "cypress/videos",
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.js",
"fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4200",
"video": false,
"retries": {
"runMode": 3,
"openMode": 0
},
"chromeWebSecurity": false
}

View File

@@ -1,164 +0,0 @@
describe('Bisq', () => {
const baseModule = Cypress.env("BASE_MODULE");
const basePath = '';
beforeEach(() => {
cy.intercept('/sockjs-node/info*').as('socket');
cy.intercept('/bisq/api/markets/hloc?market=btc_usd&interval=day').as('hloc');
cy.intercept('/bisq/api/markets/ticker').as('ticker');
cy.intercept('/bisq/api/markets/markets').as('markets');
cy.intercept('/bisq/api/markets/volumes/7d').as('7d');
cy.intercept('/bisq/api/markets/trades?market=all').as('trades');
cy.intercept('/bisq/api/txs/*/*').as('txs');
cy.intercept('/bisq/api/blocks/*/*').as('blocks');
cy.intercept('/bisq/api/stats').as('stats');
});
if (baseModule === 'bisq') {
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
});
describe("transactions", () => {
it('loads the transactions screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-transactions').click().then(() => {
cy.get('.table > tr').should('have.length', 50);
});
});
const filters = [
"Asset listing fee", "Blind vote", "Compensation request",
"Genesis", "Irregular", "Lockup", "Pay trade fee", "Proof of burn",
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
];
filters.forEach((filter) => {
it.only(`filters the transaction screen by ${filter}`, () => {
cy.visit(`${basePath}/transactions`);
cy.wait('@txs');
cy.waitForSkeletonGone();
cy.get('#filter').click();
cy.contains(filter).find('input').click();
cy.wait('@txs');
cy.wait(500);
cy.get('td:nth-of-type(2)').each(($td) => {
expect($td.text().trim()).to.eq(filter);
});
});
});
it("filters using multiple criteria", () => {
const filters = ['Proposal', 'Lockup', 'Unlock'];
cy.visit(`${basePath}/transactions`);
cy.waitForSkeletonGone();
cy.get('#filter').click();
filters.forEach((filter) => {
cy.contains(filter).find('input').click();
//TODO: change this waiter
cy.wait(1500);
});
cy.get('td:nth-of-type(2)').each(($td) => {
const regex = new RegExp(`${filters.join('|')}`, 'g');
expect($td.text().trim()).to.match(regex);
});
});
const transactions = [
{ type: 'Asset listing fee', txid: '3548aa0c002b015ea700072b7d7d76d45d4f10a3573804d0d2f624c0bb255b6b' },
{ type: 'Blind vote', txid: 'f8fabb95efa1bb81325e4c961b9fc7e3508a9b9ecd4eddf1400e58867eff8d92' },
{ type: 'Compensation request', txid: 'a8cdc65fe6bb8730f5f89f99f779d0469b0a493e1ae570e20eb7afda696a18a9' },
{ type: 'Genesis', txid: '4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5' },
{ type: 'Irregular', txid: '90b06684a517388fec2237e2362a29810dc82f0e13e019c84747ec27051e6c53' },
{ type: 'Lockup', txid: '365425b3b7487229e2ba598fb8f2a9e359e3351620383e5018548649a28b78c4' },
{ type: 'Pay trade fee', txid: 'a66b30e9777e16572ab36723539df8f45bd5d8130d810b2c3d75b8c02a191eaf' },
{ type: 'Proof of burn', txid: '8325ccb87065fb9243ed9ff1cbb431fc2ac5060a60433bcde474ccbd97b76dcb' },
{ type: 'Proposal', txid: '34e2a20f045c82fbcf7cb191b42dea6fba45641777e1751ffb29d3981c4bf413' },
{ type: 'Reimbursement request', txid: '04c16c79ca6b9ec9978880024b0d0ad3100020f33286b63c85ca7b1a319421ae' },
{ type: 'Transfer BSQ', txid: '64500bd9220675ad30d5ace27de95a341a498d7eda08162ee0ce7feb8c56cb14' },
{ type: 'Unlock', txid: '5a756841bbb11137d15b0082a3fcadbe102791f41a95d661d3bd0c5ad0b3b1a3' },
{ type: 'Vote reveal', txid: 'bd7daae1d4af8837db5e47d7bd9d8b9f83dcfd35d112f85e90728b9be45191f7' }
];
transactions.forEach((transaction) => {
it(`loads a "${transaction.type}" transaction`, () => {
cy.visit(`${basePath}/tx/${transaction.txid}`);
cy.waitForSkeletonGone();
});
});
});
describe('blocks', () => {
it('loads the blocks screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-blocks').click().then(() => {
cy.wait('@blocks');
cy.get('tbody tr').should('have.length', 10);
});
});
it('loads a specific block', () => {
cy.visit(`${basePath}/block/0000000000000000000137ef33faa63bc6e809ab30932cf77d454fb36d2bd83a`);
cy.waitForSkeletonGone();
});
});
describe('markets', () => {
it('loads the markets screen', () => {
cy.visit(`${basePath}/markets`);
cy.waitForSkeletonGone();
});
it('loads a specific market', () => {
cy.visit(`${basePath}/market/btc_eur`);
cy.waitForSkeletonGone();
//Buy Offers
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
//Sell offers
cy.get('.row > :nth-child(1) td').should('have.length.at.least', 1);
//Trades
cy.get('app-bisq-trades > .table-container td').should('have.length.at.least', 1);
});
});
it('loads the stats screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-stats').click().then(() => {
cy.wait('@stats');
});
});
it('loads the api screen', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.get('.section-header').should('have.length.at.least', 1);
cy.get('.endpoint-container').should('have.length.at.least', 1);
});
});
it('shows blocks pagination with 5 pages (desktop)', () => {
cy.viewport(760, 800);
cy.visit(`${basePath}/blocks`);
cy.waitForSkeletonGone();
cy.get('tbody tr').should('have.length', 10);
// 5 pages + 4 buttons = 9 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
});
it('shows blocks pagination with 3 pages (mobile)', () => {
cy.viewport(669, 800);
cy.visit(`${basePath}/blocks`);
cy.waitForSkeletonGone();
cy.get('tbody tr').should('have.length', 10);
// 3 pages + 4 buttons = 7 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,213 +0,0 @@
describe('Liquid', () => {
const baseModule = Cypress.env("BASE_MODULE");
const basePath = '';
beforeEach(() => {
cy.intercept('/liquid/api/block/**').as('block');
cy.intercept('/liquid/api/blocks/').as('blocks');
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
cy.intercept('/liquid/api/block/**/txs/**').as('block-txs');
cy.intercept('/resources/pools.json').as('pools');
Cypress.Commands.add('waitForBlockData', () => {
cy.wait('@socket');
cy.wait('@block');
cy.wait('@outspends');
});
});
if (baseModule === 'liquid') {
it('check first mempool block after skeleton loads', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
});
it('loads the blocks page', () => {
cy.visit(`${basePath}`);
cy.get('#btn-blocks').click().then(() => {
cy.wait(1000);
});
cy.waitForSkeletonGone();
});
it('loads a specific block page', () => {
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
cy.waitForSkeletonGone();
});
it('loads the graphs page', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.wait(1000);
});
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
});
});
it('renders unconfidential addresses correctly on mobile', () => {
cy.viewport('iphone-6');
cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`);
cy.waitForSkeletonGone();
//TODO: Add proper IDs for these selectors
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
});
});
});
describe('peg in/peg out', () => {
it('loads peg in addresses', () => {
cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`);
cy.waitForSkeletonGone();
//TODO: Change to an element id so we don't assert on a string
cy.get('.table-tx-vin').should('contain', 'Peg-in');
cy.get('.table-tx-vin a').click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
} else {
//TODO: Use an environment variable to get the hostname
cy.url().should('eq', 'http://localhost:4200/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
}
});
});
it('loads peg out addresses', () => {
cy.visit(`${basePath}/tx/ecf6eba04ffb3946faa172343c87162df76f1a57b07b0d6dc6ad956b13376dc8`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout a').first().click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/address/1BxoGcMg14oaH3CwHD2hF4gU9VcfgX5yoR');
} else {
//TODO: Use an environment variable to get the hostname
cy.url().should('eq', 'http://localhost:4200/address/1BxoGcMg14oaH3CwHD2hF4gU9VcfgX5yoR');
}
//TODO: Add a custom class so we don't assert on a string
cy.get('.badge').should('contain', 'Liquid Peg Out');
});
});
});
describe('assets', () => {
it('shows the assets screen', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.featuredBox .card').should('have.length.at.least', 5);
});
it('allows searching assets', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('ngb-typeahead-window', { timeout: 30000 }).should('have.length', 1);
});
});
it('shows a specific asset ID', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
cy.get('ngb-typeahead-window:nth-of-type(1) button', { timeout: 30000 }).click();
});
});
});
describe('unblinded TX', () => {
it('should not show an unblinding error message for regular txs', () => {
cy.visit(`${basePath}/tx/82a479043ec3841e0d3f829afc8df4f0e2bbd675a13f013ea611b2fde0027d45`);
cy.waitForSkeletonGone();
cy.get('.error-unblinded').should('not.exist');
});
it('show unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.02465000 L-BTC');
cy.get('.table-tx-vin tr').should('have.class', 'assetBox');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 L-BTC');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 L-BTC');
cy.get('.table-tx-vout tr').should('have.class', 'assetBox');
});
it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
});
it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=123`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
});
it('show first unblinded vout', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'assetBox');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 L-BTC');
});
it('show second unblinded vout', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`);
cy.get('.table-tx-vout tr:nth-child(2').should('have.class', 'assetBox');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 L-BTC');
});
it('show invalid error unblinded TX', () => {
cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3c`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout tr').should('have.class', 'assetBox');
cy.get('.error-unblinded').contains('Error: Invalid blinding data.');
});
it('shows asset peg in/out and burn transactions', () => {
cy.visit(`${basePath}/assets/asset/6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout tr').not('.assetBox');
cy.get('.table-tx-vin tr').not('.assetBox');
});
it('prevents regressing issue #644', () => {
cy.visit(`${basePath}/tx/393b890966f305e7c440fcfb12a13f51a7a9011cc59ff5f14f6f93214261bd82`);
cy.waitForSkeletonGone();
});
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,169 +0,0 @@
describe('Liquid Testnet', () => {
const baseModule = Cypress.env("BASE_MODULE");
const basePath = '/testnet';
beforeEach(() => {
cy.intercept('/liquidtestnet/api/block/**').as('block');
cy.intercept('/liquidtestnet/api/blocks/').as('blocks');
cy.intercept('/liquidtestnet/api/tx/**/outspends').as('outspends');
cy.intercept('/liquidtestnet/api/block/**/txs/**').as('block-txs');
cy.intercept('/resources/pools.json').as('pools');
Cypress.Commands.add('waitForBlockData', () => {
cy.wait('@socket');
cy.wait('@block');
cy.wait('@outspends');
});
});
if (baseModule === 'liquid') {
it('check first mempool block after skeleton loads', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
});
it('loads the blocks page', () => {
cy.visit(`${basePath}`)
cy.get('#btn-blocks');
cy.waitForSkeletonGone();
});
it('loads a specific block page', () => {
cy.visit(`${basePath}/block/7e1369a23a5ab861e7bdede2aadcccae4ea873ffd9caf11c7c5541eb5bcdff54`);
cy.waitForSkeletonGone();
});
it('loads the graphs page', () => {
cy.visit(`${basePath}`);
cy.get('#btn-graphs');
cy.waitForSkeletonGone();
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();
cy.viewport('iphone-6');
cy.get('.tv-only').should('not.exist');
});
it.skip('renders unconfidential addresses correctly on mobile', () => {
cy.viewport('iphone-6');
cy.visit(`${basePath}/address/__TODO__`);
cy.waitForSkeletonGone();
//TODO: Add proper IDs for these selectors
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
});
});
});
describe('assets', () => {
it('allows searching assets', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('ngb-typeahead-window').should('have.length', 1);
});
});
it('shows a specific asset ID', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid CAD').then(() => {
cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
});
});
});
describe('unblinded TX', () => {
it('should not show an unblinding error message for regular txs', () => {
cy.visit(`${basePath}/tx/82a479043ec3841e0d3f829afc8df4f0e2bbd675a13f013ea611b2fde0027d45`);
cy.waitForSkeletonGone();
cy.get('.error-unblinded').should('not.exist');
});
it('show unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,df290ead654d7d110ebc5aaf0bcf11d5b5d360431a467f1cde0a856fde986893,33cb3a2fd2e76643843691cf44a78c5cd28ec652a414da752160ad63fbd37bc9,49741,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,edb0713bcbfcb3daabf601cb50978439667d208e15fed8a5ebbfea5696cda1d5,4de70115501e8c7d6bd763e229bf42781edeacf6e75e1d7bdfa4c63104bc508a`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.00100000 tL-BTC');
cy.get('.table-tx-vin tr').should('have.class', 'assetBox');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00050000 tL-BTC');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.00049741 tL-BTC');
cy.get('.table-tx-vout tr').should('have.class', 'assetBox');
});
it('show empty unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', '');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', 'Confidential');
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', 'Confidential');
});
it('show invalid unblinded TX hex', () => {
cy.visit(`${basePath}/tx/2477f220eef1d03f8ffa4a2861c275d155c3562adf0d79523aeeb0c59ee611ba#blinded=5000`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', '');
cy.get('.table-tx-vout tr').should('have.class', '');
cy.get('.error-unblinded').contains('Error: Invalid blinding data (invalid hex)');
});
it('show first unblinded vout', () => {
cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'assetBox');
cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00099729 tL-BTC');
});
it('show second unblinded vout (asset)', () => {
cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`);
cy.get('.table-tx-vout tr:nth-child(2)').should('have.class', 'assetBox');
//TODO Update after the precision bug fix is merged
cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0 TEST');
});
it('should link to the asset page from the unblinded tx', () => {
cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`);
cy.get('.table-tx-vout tr:nth-child(2) .amount a').click().then(() => {
cy.waitForSkeletonGone();
cy.url().should('contain', '/assets/asset/38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5');
});
});
it('show invalid error unblinded TX', () => {
cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,df290ead654d7d110ebc5aaf0bcf11d5b5d360431a467f1cde0a856fde986893,33cb3a2fd2e76643843691cf44a78c5cd28ec652a414da752160ad63fbd37bc9,49741,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,edb0713bcbfcb3daabf601cb50978439667d208e15fed8a5ebbfea5696cda1d5,4de70115501e8c7d6bd763e229bf42781edeacf6e75e1d7bdfa4c63104bc508c`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vin tr').should('have.class', 'assetBox');
cy.get('.error-unblinded').contains('Error: Invalid blinding data.');
});
it('shows asset peg in/out and burn transactions', () => {
cy.visit(`${basePath}/assets/asset/ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout tr').not('.assetBox');
cy.get('.table-tx-vin tr').not('.assetBox');
});
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,578 +0,0 @@
import { emitMempoolInfo, dropWebSocket } from "../../support/websocket";
const baseModule = Cypress.env("BASE_MODULE");
//Credit: https://github.com/bahmutov/cypress-examples/blob/6cedb17f83a3bb03ded13cf1d6a3f0656ca2cdf5/docs/recipes/overlapping-elements.md
/**
* Returns true if two DOM rectangles are overlapping
* @param {DOMRect} rect1 the bounding client rectangle of the first element
* @param {DOMRect} rect2 the bounding client rectangle of the second element
* @returns {boolean}
*/
const areOverlapping = (rect1, rect2) => {
// if one rectangle is on the left side of the other
if (rect1.right < rect2.left || rect2.right < rect1.left) {
return false
}
// if one rectangle is above the other
if (rect1.bottom < rect2.top || rect2.bottom < rect1.top) {
return false
}
// the rectangles must overlap
return true
}
/**
* Returns the bounding rectangle of the first DOM
* element in the given jQuery object.
*/
const getRectangle = ($el) => $el[0].getBoundingClientRect();
describe('Mainnet', () => {
beforeEach(() => {
//cy.intercept('/sockjs-node/info*').as('socket');
cy.intercept('/api/block-height/*').as('block-height');
cy.intercept('/api/block/*').as('block');
cy.intercept('/api/block/*/txs/0').as('block-txs');
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
cy.intercept('/resources/pools.json').as('pools');
// Search Auto Complete
cy.intercept('/api/address-prefix/1wiz').as('search-1wiz');
cy.intercept('/api/address-prefix/1wizS').as('search-1wizS');
cy.intercept('/api/address-prefix/1wizSA').as('search-1wizSA');
Cypress.Commands.add('waitForBlockData', () => {
cy.wait('@tx-outspends');
cy.wait('@pools');
});
});
if (baseModule === 'mempool') {
it('check first mempool block after skeleton loads', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it('loads the status screen', () => {
cy.visit('/status');
cy.get('#mempool-block-0').should('be.visible');
cy.get('[id^="bitcoin-block-"]').should('have.length', 8);
cy.get('.footer').should('be.visible');
cy.get('.row > :nth-child(1)').invoke('text').then((text) => {
expect(text).to.match(/Incoming transactions.* vB\/s/);
});
cy.get('.row > :nth-child(2)').invoke('text').then((text) => {
expect(text).to.match(/Unconfirmed:(.*)/);
});
cy.get('.row > :nth-child(3)').invoke('text').then((text) => {
expect(text).to.match(/Mempool size:(.*) (kB|MB) \((\d+) (block|blocks)\)/);
});
});
//TODO: This test is flaky, refactor later
it.skip('loads dashboard, drop websocket and reconnect', () => {
cy.viewport('macbook-16');
cy.mockMempoolSocket();
cy.visit('/');
cy.get('.badge').should('not.exist');
dropWebSocket();
cy.get('.badge').should('be.visible');
cy.get('.badge', { timeout: 25000 }).should('not.exist');
emitMempoolInfo({
'params': {
command: 'init'
}
});
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
});
it('loads the dashboard', () => {
cy.visit('/');
cy.waitForSkeletonGone();
});
it('check op_return tx tooltip', () => {
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
cy.waitForSkeletonGone();
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('onmouseover');
cy.get('tbody > :nth-child(2) > :nth-child(1) > a').first().trigger('mouseenter');
cy.get('.tooltip-inner').should('be.visible');
});
it('check op_return coinbase tooltip', () => {
cy.visit('/block/00000000000000000003c5f542bed265319c6cf64238cf1f1bb9bca3ebf686d2');
cy.waitForSkeletonGone();
cy.get('div > a > .badge').first().trigger('onmouseover');
cy.get('div > a > .badge').first().trigger('mouseenter');
cy.get('.tooltip-inner').should('be.visible');
});
describe('search', () => {
it('allows searching for partial Bitcoin addresses', () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type('1wiz').then(() => {
cy.wait('@search-1wiz');
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 10);
});
cy.get('.search-box-container > .form-control').type('S').then(() => {
cy.wait('@search-1wizS');
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 5);
});
cy.get('.search-box-container > .form-control').type('A').then(() => {
cy.wait('@search-1wizSA');
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1)
});
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/1wizSAYSbuyXbt9d8JV8ytm5acqq2TorC');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
});
['BC1PQYQSZQ', 'bc1PqYqSzQ'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32m addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1);
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1pqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsyjer9e');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
});
});
});
['BC1Q000375VXCU', 'bC1q000375vXcU'].forEach((searchTerm) => {
it(`allows searching for partial case insensitive bech32 addresses: ${searchTerm}`, () => {
cy.visit('/');
cy.get('.search-box-container > .form-control').type(searchTerm).then(() => {
cy.get('ngb-typeahead-window button.dropdown-item').should('have.length', 1);
cy.get('ngb-typeahead-window button.dropdown-item.active').click().then(() => {
cy.url().should('include', '/address/bc1q000375vxcuf5v04lmwy22vy2thvhqkxghgq7dy');
cy.waitForSkeletonGone();
cy.get('.text-center').should('not.have.text', 'Invalid Bitcoin address');
});
});
});
});
});
describe('address highlighting', () => {
it('highlights single input addresses', () => {
const address = '1wiz32gbHZwMzJCRHMGehJuBgsMTPdaCa';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
cy.get('[data-cy="tx-0"] .table-tx-vin .highlight').should('exist');
cy.get('[data-cy="tx-0"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
});
it('highlights multiple input addresses', () => {
const address = '1wiz1rtKFBA58qjb582WF5KAFg9mWCuZV';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').should('exist');
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').its('length').should('equal', 2);
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
});
it('highlights both input and output addresses in the same transaction', () => {
const address = 'bc1q03u63r6hm7a3v6em58zdqtp446w2pw30nm63mv';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
cy.get('[data-cy="tx-1"] .table-tx-vin .highlight').should('exist');
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').should('exist');
});
it('highlights single output addresses', () => {
const address = '1wiz32gbHZwMzJCRHMGehJuBgsMTPdaCa';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').should('exist');
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`);
});
it('highlights multiple output addresses', () => {
const address = '1F3Q3sQmiGsWSqK5K6T9tYnX8yqzYRgQbe';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').should('exist');
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').its('length').should('equal', 2);
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`);
});
});
describe('blocks navigation', () => {
describe('keyboard events', () => {
it('loads first blockchain blocks visible and keypress arrow right', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.waitForSkeletonGone();
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="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.waitForPageIdle();
cy.document().right();
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
});
});
it('loads first blockchain blocks visible and keypress arrow left', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('.blockchain-blocks-0 > a').click().then(() => {
cy.waitForPageIdle();
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.document().left();
cy.get('.title-block h1').invoke('text').should('equal', 'Next Block');
});
});
it('loads last blockchain blocks and keypress arrow right', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('.blockchain-blocks-4 > a').click().then(() => {
cy.waitForPageIdle();
// block 6
cy.document().right();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
// block 7
cy.document().right();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
// block 8 - last visible block
cy.document().right();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
// block 9 - not visible at the blochchain blocks visible block
cy.document().right();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
});
});
it('loads genesis block and keypress arrow right', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.document().right();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
});
it('loads genesis block and keypress arrow left', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.document().left();
cy.wait(5000);
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
});
});
describe('mouse events', () => {
it('loads first blockchain blocks visible and click on the arrow right', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('.blockchain-blocks-0 > a').click().then(() => {
cy.waitForPageIdle();
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').click().then(() => {
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
});
});
});
it('loads genesis block and click on the arrow left', () => {
cy.viewport('macbook-16');
cy.visit('/block/0');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').click().then(() => {
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
});
});
});
});
it('loads skeleton when changes between networks', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.changeNetwork("testnet");
cy.changeNetwork("signet");
cy.changeNetwork("mainnet");
});
it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket();
cy.visit("/");
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(3) > #bitcoin-block-0').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('#mempool-block-1').should('be.visible');
cy.get('#mempool-block-2').should('be.visible');
emitMempoolInfo({
'params': {
command: 'init'
}
});
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
});
it('loads the pools screen', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#btn-pools').click().then(() => {
cy.wait(1000);
});
});
it('loads the graphs screen', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.wait(1000);
});
});
describe('graphs page', () => {
it('check buttons - mobile', () => {
cy.viewport('iphone-6');
cy.visit('/graphs');
cy.waitForSkeletonGone();
cy.get('.small-buttons > :nth-child(2)').should('be.visible');
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - tablet', () => {
cy.viewport('ipad-2');
cy.visit('/graphs');
cy.waitForSkeletonGone();
cy.get('.small-buttons > :nth-child(2)').should('be.visible');
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/graphs');
cy.waitForSkeletonGone();
cy.get('.small-buttons > :nth-child(2)').should('be.visible');
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
});
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('macbook-16');
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.viewport('iphone-6');
cy.visit('/tv');
cy.waitForSkeletonGone();
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('not.visible');
});
it('loads the api screen', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.wait(1000);
});
});
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/block/0000000000000000000bd14f744ef2e006e61c32214670de7eb891a5732ee775');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/block/0');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
});
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('not.be.visible');
});
});
it('shows blocks with no pagination', () => {
cy.visit('/block/00000000000000000001ba40caf1ad4cec0ceb77692662315c151953bfd7c4c4');
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('.block-tx-title h2').invoke('text').should('equal', '19 transactions');
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 5);
});
it('supports pagination on the block screen', () => {
// 41 txs
cy.visit('/block/00000000000000000009f9b7b0f63ad50053ad12ec3b7f5ca951332f134f83d8');
cy.waitForSkeletonGone();
cy.get('.pagination-container a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {
cy.waitForSkeletonGone();
cy.waitForPageIdle();
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
expect(text1).not.to.eq(text2);
});
});
});
});
it('shows blocks pagination with 5 pages (desktop)', () => {
cy.viewport(760, 800);
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
cy.waitForSkeletonGone();
cy.waitForPageIdle();
});
// 5 pages + 4 buttons = 9 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 9);
});
it('shows blocks pagination with 3 pages (mobile)', () => {
cy.viewport(669, 800);
cy.visit('/block/000000000000000000049281946d26fcba7d99fdabc1feac524bc3a7003d69b3').then(() => {
cy.waitForSkeletonGone();
cy.waitForPageIdle();
});
// 3 pages + 4 buttons = 7 buttons
cy.get('.pagination-container ul.pagination').first().children().should('have.length', 7);
});
});
describe('RBF transactions', () => {
it('shows RBF transactions properly (mobile)', () => {
cy.viewport('iphone-xr');
cy.mockMempoolSocket();
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
cy.waitForSkeletonGone();
emitMempoolInfo({
'params': {
command: 'init'
}
});
cy.get('#mempool-block-0');
emitMempoolInfo({
'params': {
command: 'rbfTransaction'
}
});
cy.get('.alert-mempool').should('be.visible');
cy.get('.alert-mempool').invoke('css', 'width').then((alertWidth) => {
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
});
cy.get('.btn-success').then(getRectangle).then((rectA) => {
cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
});
it('shows RBF transactions properly (desktop)', () => {
cy.viewport('macbook-16');
cy.mockMempoolSocket();
cy.visit('/tx/f81a08699b62b2070ad8fe0f2a076f8bea0386a2fdcd8124caee42cbc564a0d5');
cy.waitForSkeletonGone();
emitMempoolInfo({
'params': {
command: 'init'
}
});
cy.get('#mempool-block-0');
emitMempoolInfo({
'params': {
command: 'rbfTransaction'
}
});
cy.get('.alert-mempool').should('be.visible');
const alertLocator = '.alert-mempool';
const tableLocator = '.container-xl > :nth-child(3)';
cy.get(tableLocator).invoke('css', 'width').then((firstWidth) => {
cy.get(alertLocator).invoke('css', 'width').should('equal', firstWidth);
});
cy.get('.btn-success').then(getRectangle).then((rectA) => {
cy.get('.alert-mempool').then(getRectangle).then((rectB) => {
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
});
});
});
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,139 +0,0 @@
import { emitMempoolInfo } from "../../support/websocket";
const baseModule = Cypress.env("BASE_MODULE");
describe('Signet', () => {
beforeEach(() => {
cy.intercept('/api/block-height/*').as('block-height');
cy.intercept('/api/block/*').as('block');
cy.intercept('/api/block/*/txs/0').as('block-txs');
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
});
if (baseModule === 'mempool') {
it('loads the dashboard', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();
});
it('check first mempool block after skeleton loads', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket();
cy.visit("/signet");
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(3) > #bitcoin-block-0').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('#mempool-block-1').should('be.visible');
cy.get('#mempool-block-2').should('be.visible');
emitMempoolInfo({
'params': {
"network": "signet"
}
});
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
});
it('loads the pools screen', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();
cy.get('#btn-pools').click().then(() => {
cy.wait(1000);
});
});
it('loads the graphs screen', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.wait(1000);
});
});
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/signet');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.get('.chart-holder').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('.tv-only').should('not.exist');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-8');
cy.get('.chart-holder').should('be.visible');
cy.get('.tv-only').should('not.exist');
//TODO: Remove comment when the bug is fixed
//cy.get('#mempool-block-0').should('be.visible');
});
});
});
it('loads the api screen', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.wait(1000);
});
});
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/signet/block/00000133d54e4589f6436703b067ec23209e0a21b8a9b12f57d0592fd85f7a42');
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/signet/block/0');
cy.waitForSkeletonGone();
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
});
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('not.be.visible');
});
});
it('shows blocks with no pagination', () => {
cy.visit('/signet/block/00000078f920a96a69089877b934ce7fd009ab55e3170920a021262cb258e7cc');
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '13 transactions');
cy.get('ul.pagination').first().children().should('have.length', 5);
});
it('supports pagination on the block screen', () => {
// 43 txs
cy.visit('/signet/block/00000094bd52f73bdbfc4bece3a94c21fec2dc968cd54210496e69e4059d66a6');
cy.waitForSkeletonGone();
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
expect(text1).not.to.eq(text2);
});
});
});
});
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

View File

@@ -1,138 +0,0 @@
import { confirmAddress, emitMempoolInfo, sendWsMock, showNewTx, startTrackingAddress } from "../../support/websocket";
const baseModule = Cypress.env("BASE_MODULE");
describe('Testnet', () => {
beforeEach(() => {
cy.intercept('/api/block-height/*').as('block-height');
cy.intercept('/api/block/*').as('block');
cy.intercept('/api/block/*/txs/0').as('block-txs');
cy.intercept('/api/tx/*/outspends').as('tx-outspends');
});
if (baseModule === 'mempool') {
it('loads the dashboard', () => {
cy.visit('/testnet');
cy.waitForSkeletonGone();
});
it('check first mempool block after skeleton loads', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it.skip('loads the dashboard with the skeleton blocks', () => {
cy.mockMempoolSocket();
cy.visit("/testnet");
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(3) > #bitcoin-block-0').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('#mempool-block-1').should('be.visible');
cy.get('#mempool-block-2').should('be.visible');
emitMempoolInfo({
'params': {
loaded: true
}
});
cy.get(':nth-child(1) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(2) > #bitcoin-block-0').should('not.exist');
cy.get(':nth-child(3) > #bitcoin-block-0').should('not.exist');
});
it('loads the pools screen', () => {
cy.visit('/testnet');
cy.waitForSkeletonGone();
cy.get('#btn-pools').click().then(() => {
cy.wait(1000);
});
});
it('loads the graphs screen', () => {
cy.visit('/testnet');
cy.waitForSkeletonGone();
cy.get('#btn-graphs').click().then(() => {
cy.wait(1000);
});
});
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/testnet');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
//TODO: Remove comment when the bug is fixed
//cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/testnet');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
});
});
});
it('loads the api screen', () => {
cy.visit('/testnet');
cy.waitForSkeletonGone();
cy.get('#btn-docs').click().then(() => {
cy.wait(1000);
});
});
describe('blocks', () => {
it('shows empty blocks properly', () => {
cy.visit('/testnet/block/0');
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '1 transaction');
});
it('expands and collapses the block details', () => {
cy.visit('/testnet/block/0');
cy.waitForSkeletonGone();
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('be.visible');
});
cy.get('.btn.btn-outline-info').click().then(() => {
cy.get('#details').should('not.be.visible');
});
});
it('shows blocks with no pagination', () => {
cy.visit('/testnet/block/000000000000002f8ce27716e74ecc7ad9f7b5101fed12d09e28bb721b9460ea');
cy.waitForSkeletonGone();
cy.get('h2').invoke('text').should('equal', '11 transactions');
cy.get('ul.pagination').first().children().should('have.length', 5);
});
it('supports pagination on the block screen', () => {
// 48 txs
cy.visit('/testnet/block/000000000000002ca3878ebd98b313a1c2d531f2e70a6575d232ca7564dea7a9');
cy.waitForSkeletonGone();
cy.get('.header-bg.box > a').invoke('text').then((text1) => {
cy.get('.active + li').first().click().then(() => {
cy.get('.header-bg.box > a').invoke('text').then((text2) => {
expect(text1).not.to.eq(text2);
});
});
});
});
});
} else {
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
}
});

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