Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9503b2b774 | ||
|
|
ba004b7e78 | ||
|
|
9509c702db | ||
|
|
f7b4d9017a |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,3 +0,0 @@
|
||||
# ignore all differences in line endings
|
||||
package.json eol=crlf -crlf
|
||||
*/package.json eol=crlf -crlf
|
||||
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://mempool.space/about'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
37
.github/ISSUE_TEMPLATE.md
vendored
37
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,37 +0,0 @@
|
||||
<!--
|
||||
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) -->
|
||||
74
.github/workflows/on-tag.yml
vendored
74
.github/workflows/on-tag.yml
vendored
@@ -1,74 +0,0 @@
|
||||
name: Docker build on tag
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
TAG_FMT: '^refs/tags/(((.?[0-9]+){3,4}))$'
|
||||
DOCKER_BUILDKIT: 0
|
||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- frontend
|
||||
- backend
|
||||
runs-on: ubuntu-18.04
|
||||
name: Build and push to DockerHub
|
||||
steps:
|
||||
- name: Set env variables
|
||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set environment variables
|
||||
run: |
|
||||
printf " TAG: %s\n" "$TAG"
|
||||
|
||||
- name: Login to Docker for building
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx for ${{ matrix.service }} against tag
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||
--output "type=registry" ./${{ matrix.service }}/
|
||||
|
||||
- name: Run Docker buildx for ${{ matrix.service }} against latest
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
||||
--output "type=registry" ./${{ matrix.service }}/
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
||||
sitemap
|
||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN mkdir /mempool.space/
|
||||
COPY ./backend /mempool.space/backend/
|
||||
COPY ./frontend /mempool.space/frontend/
|
||||
COPY ./mariadb-structure.sql /mempool.space/mariadb-structure.sql
|
||||
|
||||
RUN apk add mariadb mariadb-client git nginx npm rsync bash
|
||||
|
||||
RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql/
|
||||
RUN /usr/bin/mysqld_safe --datadir='/var/lib/mysql/'& \
|
||||
sleep 60 && \
|
||||
mysql -e "create database mempool" && \
|
||||
mysql -e "grant all privileges on mempool.* to 'mempool'@'localhost' identified by 'mempool'" && \
|
||||
mysql mempool < /mempool.space/mariadb-structure.sql
|
||||
RUN sed -i "/^skip-networking/ c#skip-networking" /etc/my.cnf.d/mariadb-server.cnf
|
||||
|
||||
RUN export NG_CLI_ANALYTICS=ci && \
|
||||
npm install -g typescript && \
|
||||
cd /mempool.space/frontend && \
|
||||
npm install && \
|
||||
cd /mempool.space/backend && \
|
||||
npm install && \
|
||||
tsc
|
||||
|
||||
COPY ./nginx-nossl-docker.conf /etc/nginx/nginx.conf
|
||||
|
||||
ENV ENV dev
|
||||
ENV DB_HOST localhost
|
||||
ENV DB_PORT 3306
|
||||
ENV DB_USER mempool
|
||||
ENV DB_PASSWORD mempool
|
||||
ENV DB_DATABASE mempool
|
||||
ENV API_ENDPOINT /api/v1/
|
||||
ENV CHAT_SSL_ENABLED false
|
||||
ENV MEMPOOL_REFRESH_RATE_MS 500
|
||||
ENV INITIAL_BLOCK_AMOUNT 8
|
||||
ENV DEFAULT_PROJECTED_BLOCKS_AMOUNT 3
|
||||
ENV KEEP_BLOCK_AMOUNT 24
|
||||
ENV BITCOIN_NODE_HOST bitcoinhost
|
||||
ENV BITCOIN_NODE_PORT 8332
|
||||
ENV BITCOIN_NODE_USER bitcoinuser
|
||||
ENV BITCOIN_NODE_PASS bitcoinpass
|
||||
ENV TX_PER_SECOND_SPAN_SECONDS 150
|
||||
ENV BACKEND_API bitcoind
|
||||
ENV ELECTRS_API_URL https://mempool.space/api
|
||||
|
||||
RUN cd /mempool.space/frontend/ && \
|
||||
npm run build && \
|
||||
rsync -av --delete dist/mempool/ /var/www/html/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
COPY ./entrypoint.sh /mempool.space/entrypoint.sh
|
||||
RUN chmod +x /mempool.space/entrypoint.sh
|
||||
WORKDIR /mempool.space
|
||||
CMD ["/mempool.space/entrypoint.sh"]
|
||||
20
LICENSE
20
LICENSE
@@ -1,11 +1,11 @@
|
||||
MIT License with Commons Clause License Condition v1.0
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2020 The Mempool Open Source Project
|
||||
Copyright (c) 2019 Simon Lindh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
@@ -19,17 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Commons Clause License Condition v1.0
|
||||
|
||||
Without limiting other conditions in the License, the grant of rights under
|
||||
the License will not include, and the License does not grant to you, the
|
||||
right to Sell the Software.
|
||||
|
||||
For purposes of the foregoing, “Sell” means practicing any or all of the
|
||||
rights granted to you under the License to provide to third parties, for a
|
||||
fee or other consideration (including without limitation fees for hosting or
|
||||
consulting/ support services related to the Software), a product or service
|
||||
whose value derives, entirely or substantially, from the functionality of
|
||||
the Software. Any license notice or attribution required by the License must
|
||||
also include this Commons Cause License Condition notice.
|
||||
|
||||
212
README.md
212
README.md
@@ -1,41 +1,92 @@
|
||||
# The Mempool Open Source Project
|
||||
# mempool
|
||||
## a mempool visualizer and explorer for Bitcoin
|
||||
|
||||
Mempool is the fully featured mempool visualizer and block explorer website and API service running on [mempool.space](https://mempool.space/). The instructions below are for most users at home running on low-powered Raspberry Pi devices, but if you want to run a production website on a powerful server, see the [production setup guide](https://github.com/mempool/mempool/tree/master/production)
|
||||

|
||||

|
||||
|
||||

|
||||
## Pick the right version for your use case
|
||||
|
||||
# Installation
|
||||
Mempool V1 has basic explorer functionality and can run from a Bitcoin Core full node on a Raspberry Pi (no pruning, txindex=1).
|
||||
|
||||
Mempool V2 is what runs on https://mempool.space and has advanced explorer functionality, but requires a fully synced electrs backend running on powerful server hardware.
|
||||
|
||||
# Mempool V1 using Docker (easy)
|
||||
|
||||
Install from Docker Hub, passing your Bitcoin Core RPC credentials as environment variables:
|
||||
|
||||
```bash
|
||||
docker pull mempool/mempool:v1.0
|
||||
docker create -p 80:80 -e BITCOIN_NODE_HOST=192.168.1.102 -e BITCOIN_NODE_USER=foo -e BITCOIN_NODE_PASS=bar --name mempool mempool/mempool:v1.0
|
||||
docker start mempool
|
||||
docker logs mempool
|
||||
```
|
||||
|
||||
You should see mempool starting up, which takes over an hour (needs 8 blocks). When it's ready, visit http://127.0.0.1/ to see your mempool.
|
||||
|
||||
# Mempool V1 not using Docker (advanced)
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Bitcoin Core (no pruning, txindex=1)
|
||||
* Electrum Server (romanz/electrs)
|
||||
* Bitcoin (full node required, no pruning, txindex=1)
|
||||
* NodeJS (official stable LTS)
|
||||
* MariaDB (default config)
|
||||
* Nginx (use supplied nginx.conf and nginx-mempool.conf)
|
||||
* MySQL or MariaDB (default config)
|
||||
* Nginx (use supplied nginx.conf)
|
||||
|
||||
## Mempool
|
||||
|
||||
Clone the mempool repo, and checkout the latest release tag:
|
||||
## Checking out 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
|
||||
git clone https://github.com/mempool-space/mempool.space
|
||||
cd mempool.space
|
||||
git checkout v1.0.0 # put latest release tag here
|
||||
```
|
||||
|
||||
## Bitcoin Core (bitcoind)
|
||||
|
||||
Enable RPC and txindex in `bitcoin.conf`:
|
||||
Enable RPC and txindex in bitcoin.conf
|
||||
|
||||
```bash
|
||||
rpcuser=mempool
|
||||
rpcpassword=71b61986da5b03a5694d7c7d5165ece5
|
||||
txindex=1
|
||||
```
|
||||
|
||||
## NodeJS
|
||||
|
||||
Install dependencies and build code:
|
||||
|
||||
```bash
|
||||
# Install TypeScript Globally
|
||||
npm install -g typescript
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Backend
|
||||
cd ../backend/
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Mempool Configuration
|
||||
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
|
||||
"BITCOIN_NODE_HOST": "192.168.1.5",
|
||||
"BITCOIN_NODE_PORT": 8332,
|
||||
"BITCOIN_NODE_USER": "mempool",
|
||||
"BITCOIN_NODE_PASS": "71b61986da5b03a5694d7c7d5165ece5",
|
||||
```
|
||||
|
||||
## MySQL
|
||||
|
||||
Install MariaDB from OS package manager:
|
||||
Install MariaDB:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
apt-get install mariadb-server mariadb-client
|
||||
@@ -53,72 +104,51 @@ Create database and grant privileges:
|
||||
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';
|
||||
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:
|
||||
From the root folder, initialize database structure:
|
||||
|
||||
```bash
|
||||
mysql -u mempool -p mempool < mariadb-structure.sql
|
||||
```
|
||||
|
||||
## Mempool Backend
|
||||
Install mempool dependencies from npm and build the backend:
|
||||
## Running (Backend)
|
||||
|
||||
Create an initial empty cache and start the app:
|
||||
|
||||
```bash
|
||||
# backend
|
||||
cd ../backend/
|
||||
npm install
|
||||
npm run build
|
||||
touch cache.json
|
||||
npm run start # node dist/index.js
|
||||
```
|
||||
|
||||
In the `backend` folder, make a copy of the sample config and modify it to fit your settings.
|
||||
After starting you should see:
|
||||
|
||||
```bash
|
||||
cp mempool-config.sample.json mempool-config.json
|
||||
Server started on port 8999 :)
|
||||
New block found (#586498)! 0 of 1986 found in mempool. 1985 not found.
|
||||
New block found (#586499)! 0 of 1094 found in mempool. 1093 not found.
|
||||
New block found (#586500)! 0 of 2735 found in mempool. 2734 not found.
|
||||
New block found (#586501)! 0 of 2675 found in mempool. 2674 not found.
|
||||
New block found (#586502)! 0 of 975 found in mempool. 974 not found.
|
||||
New block found (#586503)! 0 of 2130 found in mempool. 2129 not found.
|
||||
New block found (#586504)! 0 of 2770 found in mempool. 2769 not found.
|
||||
New block found (#586505)! 0 of 2759 found in mempool. 2758 not found.
|
||||
Updating mempool
|
||||
Calculated fee for transaction 1 / 3257
|
||||
Calculated fee for transaction 2 / 3257
|
||||
Calculated fee for transaction 3 / 3257
|
||||
Calculated fee for transaction 4 / 3257
|
||||
Calculated fee for transaction 5 / 3257
|
||||
Calculated fee for transaction 6 / 3257
|
||||
Calculated fee for transaction 7 / 3257
|
||||
Calculated fee for transaction 8 / 3257
|
||||
Calculated fee for transaction 9 / 3257
|
||||
```
|
||||
|
||||
Edit `mempool-config.json` to add your Bitcoin Core node RPC credentials:
|
||||
```bash
|
||||
{
|
||||
"MEMPOOL": {
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "electrum",
|
||||
"HTTP_PORT": 8999,
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"POLL_RATE_MS": 2000
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "71b61986da5b03a5694d7c7d5165ece5"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"STATISTICS": {
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Start the backend:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
When it's running you should see output like this:
|
||||
You need to wait for at least *8 blocks to be mined*, so please wait ~80 minutes.
|
||||
The backend also needs to index transactions, calculate fees, etc.
|
||||
When it's ready you will see output like this:
|
||||
|
||||
```bash
|
||||
Mempool updated in 0.189 seconds
|
||||
@@ -141,38 +171,36 @@ When it's running you should see output like this:
|
||||
Updating mempool
|
||||
```
|
||||
|
||||
## Mempool Frontend
|
||||
|
||||
Install mempool dependencies from npm and build the frontend static HTML/CSS/JS:
|
||||
|
||||
```bash
|
||||
# frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Install the output into nginx webroot folder:
|
||||
|
||||
```bash
|
||||
sudo rsync -av --delete dist/mempool/ /var/www/html/
|
||||
```
|
||||
|
||||
## nginx + certbot
|
||||
|
||||
Install the supplied nginx.conf and nginx-mempool.conf in /etc/nginx
|
||||
## nginx + CertBot (LetsEncrypt)
|
||||
Setup nginx using the supplied nginx.conf
|
||||
|
||||
```bash
|
||||
# install nginx and certbot
|
||||
apt-get install -y nginx python-certbot-nginx
|
||||
|
||||
# install the mempool configuration for nginx
|
||||
cp nginx.conf nginx-mempool.conf /etc/nginx/nginx.conf
|
||||
|
||||
# replace example.com with your domain name
|
||||
certbot --nginx -d example.com
|
||||
|
||||
# install the mempool configuration for nginx
|
||||
cp nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# edit the installed nginx.conf, and replace all
|
||||
# instances of example.com with your domain name
|
||||
```
|
||||
Make sure you can access https://<your-domain-name>/ in browser before proceeding
|
||||
|
||||
|
||||
## Running (Frontend)
|
||||
|
||||
Build the frontend static HTML/CSS/JS, rsync the output into nginx folder:
|
||||
|
||||
```bash
|
||||
cd frontend/
|
||||
npm run build
|
||||
sudo rsync -av --delete dist/mempool/ /var/www/html/
|
||||
```
|
||||
|
||||
## Try It Out
|
||||
|
||||
If everything went okay you should see the beautiful mempool :grin:
|
||||
|
||||
|
||||
9
backend/.gitignore
vendored
9
backend/.gitignore
vendored
@@ -43,12 +43,3 @@ testem.log
|
||||
Thumbs.db
|
||||
|
||||
cache.json
|
||||
cache1.json
|
||||
cache2.json
|
||||
cache3.json
|
||||
cache4.json
|
||||
cache5.json
|
||||
cache6.json
|
||||
cache7.json
|
||||
cache8.json
|
||||
cache9.json
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
FROM node:12-buster-slim AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
|
||||
RUN sed -i "s!../.git/refs/heads/master!master!g" ./src/api/backend-info.ts
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential python3 pkg-config
|
||||
RUN npm ci --production
|
||||
RUN npm i typescript
|
||||
RUN npm run build
|
||||
|
||||
RUN mv ./docker/* .
|
||||
RUN mv ./mempool-config-docker.json ./mempool-config.json
|
||||
|
||||
FROM node:12-buster-slim
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
COPY --from=builder /build/ .
|
||||
|
||||
RUN chmod +x /backend/start.sh
|
||||
RUN chmod +x /backend/wait-for-it.sh
|
||||
|
||||
RUN chown -R 1000:1000 /backend && chmod -R 755 /backend
|
||||
|
||||
USER 1000
|
||||
|
||||
EXPOSE 8999
|
||||
|
||||
CMD ["/backend/start.sh"]
|
||||
@@ -1 +0,0 @@
|
||||
9d02ab1eb5ffb60d38128df903e47e11b95f13d5
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"MEMPOOL": {
|
||||
"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__"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__BITCOIN_MAINNET_RPC_HOST__",
|
||||
"PORT": __BITCOIN_MAINNET_RPC_PORT__,
|
||||
"USERNAME": "__BITCOIN_MAINNET_RPC_USER__",
|
||||
"PASSWORD": "__BITCOIN_MAINNET_RPC_PASS__"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRS_MAINNET_HTTP_HOST__",
|
||||
"PORT": __ELECTRS_MAINNET_HTTP_PORT__,
|
||||
"TLS_ENABLED": false,
|
||||
"TX_LOOKUPS": true
|
||||
},
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000"
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": true,
|
||||
"HOST": "__MYSQL_HOST__",
|
||||
"PORT": __MYSQL_PORT__,
|
||||
"DATABASE": "mempool",
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
},
|
||||
"STATISTICS": {
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#MEMPOOL
|
||||
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
|
||||
__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__=${CACHE_DIR:=./}
|
||||
# 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
|
||||
__ELECTRS_MAINNET_HTTP_HOST__=${ELECTRS_HOST:=127.0.0.1}
|
||||
__ELECTRS_MAINNET_HTTP_PORT__=${ELECTRS_PORT:=50002}
|
||||
# MYSQL
|
||||
__MYSQL_HOST__=${MYSQL_HOST:=127.0.0.1}
|
||||
__MYSQL_PORT__=${MYSQL_PORT:=3306}
|
||||
|
||||
mkdir -p "${__MEMPOOL_BACKEND_MAINNET_CACHE_DIR__}"
|
||||
|
||||
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/__ELECTRS_MAINNET_HTTP_HOST__/${__ELECTRS_MAINNET_HTTP_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__ELECTRS_MAINNET_HTTP_PORT__/${__ELECTRS_MAINNET_HTTP_PORT__}/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!__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
|
||||
|
||||
node /backend/dist/index.js
|
||||
@@ -1,182 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Use this script to test if a given TCP host/port are available
|
||||
|
||||
WAITFORIT_cmdname=${0##*/}
|
||||
|
||||
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
|
||||
|
||||
usage()
|
||||
{
|
||||
cat << USAGE >&2
|
||||
Usage:
|
||||
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
|
||||
-h HOST | --host=HOST Host or IP under test
|
||||
-p PORT | --port=PORT TCP port under test
|
||||
Alternatively, you specify the host and port as host:port
|
||||
-s | --strict Only execute subcommand if the test succeeds
|
||||
-q | --quiet Don't output any status messages
|
||||
-t TIMEOUT | --timeout=TIMEOUT
|
||||
Timeout in seconds, zero for no timeout
|
||||
-- COMMAND ARGS Execute command with args after the test finishes
|
||||
USAGE
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for()
|
||||
{
|
||||
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||
else
|
||||
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
|
||||
fi
|
||||
WAITFORIT_start_ts=$(date +%s)
|
||||
while :
|
||||
do
|
||||
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
|
||||
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
|
||||
WAITFORIT_result=$?
|
||||
else
|
||||
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
|
||||
WAITFORIT_result=$?
|
||||
fi
|
||||
if [[ $WAITFORIT_result -eq 0 ]]; then
|
||||
WAITFORIT_end_ts=$(date +%s)
|
||||
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return $WAITFORIT_result
|
||||
}
|
||||
|
||||
wait_for_wrapper()
|
||||
{
|
||||
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
|
||||
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
|
||||
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||
else
|
||||
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||
fi
|
||||
WAITFORIT_PID=$!
|
||||
trap "kill -INT -$WAITFORIT_PID" INT
|
||||
wait $WAITFORIT_PID
|
||||
WAITFORIT_RESULT=$?
|
||||
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||
fi
|
||||
return $WAITFORIT_RESULT
|
||||
}
|
||||
|
||||
# process arguments
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case "$1" in
|
||||
*:* )
|
||||
WAITFORIT_hostport=(${1//:/ })
|
||||
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
|
||||
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
|
||||
shift 1
|
||||
;;
|
||||
--child)
|
||||
WAITFORIT_CHILD=1
|
||||
shift 1
|
||||
;;
|
||||
-q | --quiet)
|
||||
WAITFORIT_QUIET=1
|
||||
shift 1
|
||||
;;
|
||||
-s | --strict)
|
||||
WAITFORIT_STRICT=1
|
||||
shift 1
|
||||
;;
|
||||
-h)
|
||||
WAITFORIT_HOST="$2"
|
||||
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--host=*)
|
||||
WAITFORIT_HOST="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
-p)
|
||||
WAITFORIT_PORT="$2"
|
||||
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--port=*)
|
||||
WAITFORIT_PORT="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
-t)
|
||||
WAITFORIT_TIMEOUT="$2"
|
||||
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--timeout=*)
|
||||
WAITFORIT_TIMEOUT="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
WAITFORIT_CLI=("$@")
|
||||
break
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echoerr "Unknown argument: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
|
||||
echoerr "Error: you need to provide a host and port to test."
|
||||
usage
|
||||
fi
|
||||
|
||||
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
|
||||
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
|
||||
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
|
||||
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
|
||||
|
||||
# Check to see if timeout is from busybox?
|
||||
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
|
||||
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
|
||||
|
||||
WAITFORIT_BUSYTIMEFLAG=""
|
||||
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
|
||||
WAITFORIT_ISBUSY=1
|
||||
# Check if busybox timeout uses -t flag
|
||||
# (recent Alpine versions don't support -t anymore)
|
||||
if timeout &>/dev/stdout | grep -q -e '-t '; then
|
||||
WAITFORIT_BUSYTIMEFLAG="-t"
|
||||
fi
|
||||
else
|
||||
WAITFORIT_ISBUSY=0
|
||||
fi
|
||||
|
||||
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
|
||||
wait_for
|
||||
WAITFORIT_RESULT=$?
|
||||
exit $WAITFORIT_RESULT
|
||||
else
|
||||
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||
wait_for_wrapper
|
||||
WAITFORIT_RESULT=$?
|
||||
else
|
||||
wait_for
|
||||
WAITFORIT_RESULT=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $WAITFORIT_CLI != "" ]]; then
|
||||
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
|
||||
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
|
||||
exit $WAITFORIT_RESULT
|
||||
fi
|
||||
exec "${WAITFORIT_CLI[@]}"
|
||||
else
|
||||
exit $WAITFORIT_RESULT
|
||||
fi
|
||||
@@ -1,59 +1,25 @@
|
||||
{
|
||||
"MEMPOOL": {
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "electrum",
|
||||
"HTTP_PORT": 8999,
|
||||
"SPAWN_CLUSTER_PROCS": 0,
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"POLL_RATE_MS": 2000,
|
||||
"CACHE_DIR": "./"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 50002,
|
||||
"TLS_ENABLED": true
|
||||
},
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000"
|
||||
},
|
||||
"CORE_RPC_MINFEE": {
|
||||
"ENABLED": false,
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
},
|
||||
"DATABASE": {
|
||||
"ENABLED": true,
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 3306,
|
||||
"DATABASE": "mempool",
|
||||
"USERNAME": "mempool",
|
||||
"PASSWORD": "mempool"
|
||||
},
|
||||
"STATISTICS": {
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
},
|
||||
"BISQ_BLOCKS": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db/json"
|
||||
},
|
||||
"BISQ_MARKETS": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
},
|
||||
"SPONSORS": {
|
||||
"ENABLED": false,
|
||||
"BTCPAY_URL": "",
|
||||
"BTCPAY_AUTH": "",
|
||||
"BTCPAY_WEBHOOK_URL": "",
|
||||
"TWITTER_BEARER_AUTH": ""
|
||||
}
|
||||
"ENV": "dev",
|
||||
"DB_HOST": "localhost",
|
||||
"DB_PORT": 3306,
|
||||
"DB_USER": "mempool",
|
||||
"DB_PASSWORD": "mempool",
|
||||
"DB_DATABASE": "mempool",
|
||||
"HTTP_PORT": 3000,
|
||||
"API_ENDPOINT": "/api/v1/",
|
||||
"CHAT_SSL_ENABLED": false,
|
||||
"CHAT_SSL_PRIVKEY": "",
|
||||
"CHAT_SSL_CERT": "",
|
||||
"CHAT_SSL_CHAIN": "",
|
||||
"MEMPOOL_REFRESH_RATE_MS": 500,
|
||||
"INITIAL_BLOCK_AMOUNT": 8,
|
||||
"DEFAULT_PROJECTED_BLOCKS_AMOUNT": 3,
|
||||
"KEEP_BLOCK_AMOUNT": 24,
|
||||
"BITCOIN_NODE_HOST": "localhost",
|
||||
"BITCOIN_NODE_PORT": 8332,
|
||||
"BITCOIN_NODE_USER": "",
|
||||
"BITCOIN_NODE_PASS": "",
|
||||
"BACKEND_API": "bitcoind",
|
||||
"ELECTRS_API_URL": "https://mempool.space/api",
|
||||
"TX_PER_SECOND_SPAN_SECONDS": 150
|
||||
}
|
||||
|
||||
2847
backend/package-lock.json
generated
2847
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,31 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.0.0",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "MIT",
|
||||
"homepage": "https://mempool.space",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mempool/mempool"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/mempool/mempool/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"bitcoin",
|
||||
"mempool",
|
||||
"blockchain",
|
||||
"explorer",
|
||||
"liquid"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"description": "Bitcoin Mempool Visualizer",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
||||
"tsc": "./node_modules/typescript/bin/tsc",
|
||||
"build": "npm run tsc",
|
||||
"start": "node --max-old-space-size=4096 dist/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "tsc",
|
||||
"start": "npm run build && node dist/index.js"
|
||||
},
|
||||
"author": {
|
||||
"name": "Simon Lindh",
|
||||
"url": "https://github.com/mempool-space/mempool.space"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mempool/bitcoin": "^3.0.2",
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
"axios": "^0.21.1",
|
||||
"bitcoinjs-lib": "^5.2.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
"express": "^4.17.1",
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "2.2.5",
|
||||
"node-worker-threads-pool": "^1.4.2",
|
||||
"ws": "^7.3.1"
|
||||
"bitcoin": "^3.0.1",
|
||||
"compression": "^1.7.3",
|
||||
"express": "^4.16.3",
|
||||
"mysql2": "^1.6.1",
|
||||
"request": "^2.88.0",
|
||||
"ws": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.0.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/locutus": "^0.0.6",
|
||||
"@types/ws": "^6.0.4",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~3.9.7"
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/mysql2": "github:types/mysql2",
|
||||
"@types/request": "^2.48.2",
|
||||
"@types/ws": "^6.0.1",
|
||||
"tslint": "^5.11.0",
|
||||
"typescript": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import logger from '../logger';
|
||||
|
||||
class BackendInfo {
|
||||
gitCommitHash = '';
|
||||
hostname = '';
|
||||
|
||||
constructor() {
|
||||
this.setLatestCommitHash();
|
||||
this.hostname = os.hostname();
|
||||
}
|
||||
|
||||
public getBackendInfo() {
|
||||
return {
|
||||
'hostname': this.hostname,
|
||||
'git-commit': this.gitCommitHash,
|
||||
};
|
||||
}
|
||||
|
||||
public getShortCommitHash() {
|
||||
return this.gitCommitHash.slice(0, 7);
|
||||
}
|
||||
|
||||
private setLatestCommitHash(): void {
|
||||
try {
|
||||
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||
} catch (e) {
|
||||
logger.err('Could not load git commit info: ' + e.message || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BackendInfo();
|
||||
@@ -1,275 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as fs from 'fs';
|
||||
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 logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static BLOCKS_JSON_FILE_PATH = config.BISQ_BLOCKS.DATA_PATH + '/all/blocks.json';
|
||||
private latestBlockHeight = 0;
|
||||
private blocks: BisqBlock[] = [];
|
||||
private transactions: BisqTransaction[] = [];
|
||||
private transactionIndex: { [txId: string]: BisqTransaction } = {};
|
||||
private blockIndex: { [hash: string]: BisqBlock } = {};
|
||||
private addressIndex: { [address: string]: BisqTransaction[] } = {};
|
||||
private stats: BisqStats = {
|
||||
minted: 0,
|
||||
burnt: 0,
|
||||
addresses: 0,
|
||||
unspent_txos: 0,
|
||||
spent_txos: 0,
|
||||
};
|
||||
private price: number = 0;
|
||||
private priceUpdateCallbackFunction: ((price: number) => void) | undefined;
|
||||
private topDirectoryWatcher: fs.FSWatcher | undefined;
|
||||
private subdirectoryWatcher: fs.FSWatcher | undefined;
|
||||
private jsonParsePool = new StaticPool({
|
||||
size: 4,
|
||||
task: (blob: string) => JSON.parse(blob),
|
||||
});
|
||||
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
this.checkForBisqDataFolder();
|
||||
this.loadBisqDumpFile();
|
||||
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
|
||||
this.updatePrice();
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
}
|
||||
|
||||
handleNewBitcoinBlock(block: BlockExtended): void {
|
||||
if (block.height - 10 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
|
||||
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
}
|
||||
}
|
||||
|
||||
getTransaction(txId: string): BisqTransaction | undefined {
|
||||
return this.transactionIndex[txId];
|
||||
}
|
||||
|
||||
getTransactions(start: number, length: number, types: string[]): [BisqTransaction[], number] {
|
||||
let transactions = this.transactions;
|
||||
if (types.length) {
|
||||
transactions = transactions.filter((tx) => types.indexOf(tx.txType) > -1);
|
||||
}
|
||||
return [transactions.slice(start, length + start), transactions.length];
|
||||
}
|
||||
|
||||
getBlock(hash: string): BisqBlock | undefined {
|
||||
return this.blockIndex[hash];
|
||||
}
|
||||
|
||||
getAddress(hash: string): BisqTransaction[] {
|
||||
return this.addressIndex[hash];
|
||||
}
|
||||
|
||||
getBlocks(start: number, length: number): [BisqBlock[], number] {
|
||||
return [this.blocks.slice(start, length + start), this.blocks.length];
|
||||
}
|
||||
|
||||
getStats(): BisqStats {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
setPriceCallbackFunction(fn: (price: number) => void) {
|
||||
this.priceUpdateCallbackFunction = fn;
|
||||
}
|
||||
|
||||
getLatestBlockHeight(): number {
|
||||
return this.latestBlockHeight;
|
||||
}
|
||||
|
||||
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.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private startTopDirectoryWatcher() {
|
||||
if (this.topDirectoryWatcher) {
|
||||
this.topDirectoryWatcher.close();
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.topDirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH, () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Bisq restart detected. Resetting both watchers in 3 minutes.`);
|
||||
setTimeout(() => {
|
||||
this.startTopDirectoryWatcher();
|
||||
this.startSubDirectoryWatcher();
|
||||
this.loadBisqDumpFile();
|
||||
}, 180000);
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
private startSubDirectoryWatcher() {
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
|
||||
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
|
||||
setTimeout(() => this.startSubDirectoryWatcher(), 180000);
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(config.BISQ_BLOCKS.DATA_PATH + '/all', () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Change detected in the Bisq data folder.`);
|
||||
this.loadBisqDumpFile();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
private updatePrice() {
|
||||
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
|
||||
.then((response) => {
|
||||
const prices: number[] = [];
|
||||
response.data.forEach((trade) => {
|
||||
prices.push(parseFloat(trade.price) * 100000000);
|
||||
});
|
||||
prices.sort((a, b) => a - b);
|
||||
this.price = Common.median(prices);
|
||||
if (this.priceUpdateCallbackFunction) {
|
||||
this.priceUpdateCallbackFunction(this.price);
|
||||
}
|
||||
}).catch((err) => {
|
||||
logger.err('Error updating Bisq market price: ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadBisqDumpFile(): Promise<void> {
|
||||
try {
|
||||
const data = await this.loadData();
|
||||
await this.loadBisqBlocksDump(data);
|
||||
this.buildIndex();
|
||||
this.calculateStats();
|
||||
} catch (e) {
|
||||
logger.err('loadBisqDumpFile() error.' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private buildIndex() {
|
||||
const start = new Date().getTime();
|
||||
this.transactions = [];
|
||||
this.transactionIndex = {};
|
||||
this.addressIndex = {};
|
||||
|
||||
this.blocks.forEach((block) => {
|
||||
/* Build block index */
|
||||
if (!this.blockIndex[block.hash]) {
|
||||
this.blockIndex[block.hash] = block;
|
||||
}
|
||||
|
||||
/* Build transactions index */
|
||||
block.txs.forEach((tx) => {
|
||||
this.transactions.push(tx);
|
||||
this.transactionIndex[tx.id] = tx;
|
||||
});
|
||||
});
|
||||
|
||||
/* Build address index */
|
||||
this.transactions.forEach((tx) => {
|
||||
tx.inputs.forEach((input) => {
|
||||
if (!this.addressIndex[input.address]) {
|
||||
this.addressIndex[input.address] = [];
|
||||
}
|
||||
if (this.addressIndex[input.address].indexOf(tx) === -1) {
|
||||
this.addressIndex[input.address].push(tx);
|
||||
}
|
||||
});
|
||||
tx.outputs.forEach((output) => {
|
||||
if (!this.addressIndex[output.address]) {
|
||||
this.addressIndex[output.address] = [];
|
||||
}
|
||||
if (this.addressIndex[output.address].indexOf(tx) === -1) {
|
||||
this.addressIndex[output.address].push(tx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq data index rebuilt in ' + time + ' ms');
|
||||
}
|
||||
|
||||
private calculateStats() {
|
||||
let minted = 0;
|
||||
let burned = 0;
|
||||
let unspent = 0;
|
||||
let spent = 0;
|
||||
|
||||
this.transactions.forEach((tx) => {
|
||||
tx.outputs.forEach((output) => {
|
||||
if (output.opReturn) {
|
||||
return;
|
||||
}
|
||||
if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) {
|
||||
minted += output.bsqAmount;
|
||||
}
|
||||
if (output.isUnspent) {
|
||||
unspent++;
|
||||
} else {
|
||||
spent++;
|
||||
}
|
||||
});
|
||||
burned += tx['burntFee'];
|
||||
});
|
||||
|
||||
this.stats = {
|
||||
addresses: Object.keys(this.addressIndex).length,
|
||||
minted: minted / 100,
|
||||
burnt: burned / 100,
|
||||
spent_txos: spent,
|
||||
unspent_txos: unspent,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadBisqBlocksDump(cacheData: string): Promise<void> {
|
||||
const start = new Date().getTime();
|
||||
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.blocks.length) {
|
||||
this.blocks = data.blocks.filter((block) => block.txs.length > 0);
|
||||
this.blocks.reverse();
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Bisq();
|
||||
@@ -1,258 +0,0 @@
|
||||
|
||||
export interface BisqBlocks {
|
||||
chainHeight: number;
|
||||
blocks: BisqBlock[];
|
||||
}
|
||||
|
||||
export interface BisqBlock {
|
||||
height: number;
|
||||
time: number;
|
||||
hash: string;
|
||||
previousBlockHash: string;
|
||||
txs: BisqTransaction[];
|
||||
}
|
||||
|
||||
export interface BisqTransaction {
|
||||
txVersion: string;
|
||||
id: string;
|
||||
blockHeight: number;
|
||||
blockHash: string;
|
||||
time: number;
|
||||
inputs: BisqInput[];
|
||||
outputs: BisqOutput[];
|
||||
txType: string;
|
||||
txTypeDisplayString: string;
|
||||
burntFee: number;
|
||||
invalidatedBsq: number;
|
||||
unlockBlockHeight: number;
|
||||
}
|
||||
|
||||
export interface BisqStats {
|
||||
minted: number;
|
||||
burnt: number;
|
||||
addresses: number;
|
||||
unspent_txos: number;
|
||||
spent_txos: number;
|
||||
}
|
||||
|
||||
interface BisqInput {
|
||||
spendingTxOutputIndex: number;
|
||||
spendingTxId: string;
|
||||
bsqAmount: number;
|
||||
isVerified: boolean;
|
||||
address: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
interface BisqOutput {
|
||||
txVersion: string;
|
||||
txId: string;
|
||||
index: number;
|
||||
bsqAmount: number;
|
||||
btcAmount: number;
|
||||
height: number;
|
||||
isVerified: boolean;
|
||||
burntFee: number;
|
||||
invalidatedBsq: number;
|
||||
address: string;
|
||||
scriptPubKey: BisqScriptPubKey;
|
||||
time: any;
|
||||
txType: string;
|
||||
txTypeDisplayString: string;
|
||||
txOutputType: string;
|
||||
txOutputTypeDisplayString: string;
|
||||
lockTime: number;
|
||||
isUnspent: boolean;
|
||||
spentInfo: SpentInfo;
|
||||
opReturn?: string;
|
||||
}
|
||||
|
||||
interface BisqScriptPubKey {
|
||||
addresses: string[];
|
||||
asm: string;
|
||||
hex: string;
|
||||
reqSigs: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SpentInfo {
|
||||
height: number;
|
||||
inputIndex: number;
|
||||
txId: string;
|
||||
}
|
||||
|
||||
export interface BisqTrade {
|
||||
direction: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
market?: string;
|
||||
}
|
||||
|
||||
export interface Currencies { [txid: string]: Currency; }
|
||||
|
||||
export interface Currency {
|
||||
code: string;
|
||||
name: string;
|
||||
precision: number;
|
||||
|
||||
_type: string;
|
||||
}
|
||||
|
||||
export interface Depth { [market: string]: Market; }
|
||||
|
||||
interface Market {
|
||||
'buys': string[];
|
||||
'sells': string[];
|
||||
}
|
||||
|
||||
export interface HighLowOpenClose {
|
||||
period_start: number | string;
|
||||
open: string;
|
||||
high: string;
|
||||
low: string;
|
||||
close: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
avg: string;
|
||||
}
|
||||
|
||||
export interface Markets { [txid: string]: Pair; }
|
||||
|
||||
interface Pair {
|
||||
pair: string;
|
||||
lname: string;
|
||||
rname: string;
|
||||
lsymbol: string;
|
||||
rsymbol: string;
|
||||
lprecision: number;
|
||||
rprecision: number;
|
||||
ltype: string;
|
||||
rtype: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Offers { [market: string]: OffersMarket; }
|
||||
|
||||
interface OffersMarket {
|
||||
buys: Offer[] | null;
|
||||
sells: Offer[] | null;
|
||||
}
|
||||
|
||||
export interface OffersData {
|
||||
direction: string;
|
||||
currencyCode: string;
|
||||
minAmount: number;
|
||||
amount: number;
|
||||
price: number;
|
||||
date: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
paymentMethod: string;
|
||||
id: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
priceDisplayString: string;
|
||||
primaryMarketAmountDisplayString: string;
|
||||
primaryMarketMinAmountDisplayString: string;
|
||||
primaryMarketVolumeDisplayString: string;
|
||||
primaryMarketMinVolumeDisplayString: string;
|
||||
primaryMarketPrice: number;
|
||||
primaryMarketAmount: number;
|
||||
primaryMarketMinAmount: number;
|
||||
primaryMarketVolume: number;
|
||||
primaryMarketMinVolume: number;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
offer_id: string;
|
||||
offer_date: number;
|
||||
direction: string;
|
||||
min_amount: string;
|
||||
amount: string;
|
||||
price: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
offer_fee_txid: any;
|
||||
}
|
||||
|
||||
export interface Tickers { [market: string]: Ticker | null; }
|
||||
|
||||
export interface Ticker {
|
||||
last: string;
|
||||
high: string;
|
||||
low: string;
|
||||
volume_left: string;
|
||||
volume_right: string;
|
||||
buy: string | null;
|
||||
sell: string | null;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
direction: string;
|
||||
price: string;
|
||||
amount: string;
|
||||
volume: string;
|
||||
payment_method: string;
|
||||
trade_id: string;
|
||||
trade_date: number;
|
||||
}
|
||||
|
||||
export interface TradesData {
|
||||
currency: string;
|
||||
direction: string;
|
||||
tradePrice: number;
|
||||
tradeAmount: number;
|
||||
tradeDate: number;
|
||||
paymentMethod: string;
|
||||
offerDate: number;
|
||||
useMarketBasedPrice: boolean;
|
||||
marketPriceMargin: number;
|
||||
offerAmount: number;
|
||||
offerMinAmount: number;
|
||||
offerId: string;
|
||||
depositTxId?: string;
|
||||
currencyPair: string;
|
||||
primaryMarketDirection: string;
|
||||
primaryMarketTradePrice: number;
|
||||
primaryMarketTradeAmount: number;
|
||||
primaryMarketTradeVolume: number;
|
||||
|
||||
_market: string;
|
||||
_tradePriceStr: string;
|
||||
_tradeAmountStr: string;
|
||||
_tradeVolumeStr: string;
|
||||
_offerAmountStr: string;
|
||||
_tradePrice: number;
|
||||
_tradeAmount: number;
|
||||
_tradeVolume: number;
|
||||
_offerAmount: number;
|
||||
}
|
||||
|
||||
export interface MarketVolume {
|
||||
period_start: number;
|
||||
num_trades: number;
|
||||
volume: string;
|
||||
}
|
||||
|
||||
export interface MarketsApiError {
|
||||
success: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
|
||||
|
||||
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
|
||||
export interface SummarizedInterval {
|
||||
'period_start': number;
|
||||
'open': number;
|
||||
'close': number;
|
||||
'high': number;
|
||||
'low': number;
|
||||
'avg': number;
|
||||
'volume_right': number;
|
||||
'volume_left': number;
|
||||
}
|
||||
@@ -1,655 +0,0 @@
|
||||
import { Currencies, OffersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
|
||||
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
|
||||
|
||||
import * as datetime from 'locutus/php/datetime';
|
||||
|
||||
class BisqMarketsApi {
|
||||
private cryptoCurrencyData: Currency[] = [];
|
||||
private fiatCurrencyData: Currency[] = [];
|
||||
private activeCryptoCurrencyData: Currency[] = [];
|
||||
private activeFiatCurrencyData: Currency[] = [];
|
||||
private offersData: OffersData[] = [];
|
||||
private tradesData: TradesData[] = [];
|
||||
private fiatCurrenciesIndexed: { [code: string]: true } = {};
|
||||
private allCurrenciesIndexed: { [code: string]: Currency } = {};
|
||||
private tradeDataByMarket: { [market: string]: TradesData[] } = {};
|
||||
private tickersCache: Ticker | Tickers | null = null;
|
||||
|
||||
constructor() { }
|
||||
|
||||
setOffersData(offers: OffersData[]) {
|
||||
this.offersData = offers;
|
||||
}
|
||||
|
||||
setTradesData(trades: TradesData[]) {
|
||||
this.tradesData = trades;
|
||||
this.tradeDataByMarket = {};
|
||||
|
||||
this.tradesData.forEach((trade) => {
|
||||
trade._market = trade.currencyPair.toLowerCase().replace('/', '_');
|
||||
if (!this.tradeDataByMarket[trade._market]) {
|
||||
this.tradeDataByMarket[trade._market] = [];
|
||||
}
|
||||
this.tradeDataByMarket[trade._market].push(trade);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrencyData(cryptoCurrency: Currency[], fiatCurrency: Currency[], activeCryptoCurrency: Currency[], activeFiatCurrency: Currency[]) {
|
||||
this.cryptoCurrencyData = cryptoCurrency,
|
||||
this.fiatCurrencyData = fiatCurrency,
|
||||
this.activeCryptoCurrencyData = activeCryptoCurrency,
|
||||
this.activeFiatCurrencyData = activeFiatCurrency;
|
||||
|
||||
this.fiatCurrenciesIndexed = {};
|
||||
this.allCurrenciesIndexed = {};
|
||||
|
||||
this.fiatCurrencyData.forEach((currency) => {
|
||||
currency._type = 'fiat';
|
||||
this.fiatCurrenciesIndexed[currency.code] = true;
|
||||
this.allCurrenciesIndexed[currency.code] = currency;
|
||||
});
|
||||
this.cryptoCurrencyData.forEach((currency) => {
|
||||
currency._type = 'crypto';
|
||||
this.allCurrenciesIndexed[currency.code] = currency;
|
||||
});
|
||||
}
|
||||
|
||||
updateCache() {
|
||||
this.tickersCache = null;
|
||||
this.tickersCache = this.getTicker();
|
||||
}
|
||||
|
||||
getCurrencies(
|
||||
type: 'crypto' | 'fiat' | 'active' | 'all' = 'all',
|
||||
): Currencies {
|
||||
let currencies: Currency[];
|
||||
|
||||
switch (type) {
|
||||
case 'fiat':
|
||||
currencies = this.fiatCurrencyData;
|
||||
break;
|
||||
case 'crypto':
|
||||
currencies = this.cryptoCurrencyData;
|
||||
break;
|
||||
case 'active':
|
||||
currencies = this.activeCryptoCurrencyData.concat(this.activeFiatCurrencyData);
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData);
|
||||
}
|
||||
const result = {};
|
||||
currencies.forEach((currency) => {
|
||||
result[currency.code] = currency;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
getDepth(
|
||||
market: string,
|
||||
): Depth {
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
|
||||
const buys = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
|
||||
.map((offer) => offer.price)
|
||||
.sort((a, b) => b - a)
|
||||
.map((price) => this.intToBtc(price));
|
||||
|
||||
const sells = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
|
||||
.map((offer) => offer.price)
|
||||
.sort((a, b) => a - b)
|
||||
.map((price) => this.intToBtc(price));
|
||||
|
||||
const result = {};
|
||||
result[market] = {
|
||||
'buys': buys,
|
||||
'sells': sells,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
getOffers(
|
||||
market: string,
|
||||
direction?: 'buy' | 'sell',
|
||||
): Offers {
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
|
||||
let buys: Offer[] | null = null;
|
||||
let sells: Offer[] | null = null;
|
||||
|
||||
if (!direction || direction === 'buy') {
|
||||
buys = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
|
||||
.sort((a, b) => b.price - a.price)
|
||||
.map((offer) => this.offerDataToOffer(offer, market));
|
||||
}
|
||||
|
||||
if (!direction || direction === 'sell') {
|
||||
sells = this.offersData
|
||||
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
|
||||
.sort((a, b) => a.price - b.price)
|
||||
.map((offer) => this.offerDataToOffer(offer, market));
|
||||
}
|
||||
|
||||
const result: Offers = {};
|
||||
result[market] = {
|
||||
'buys': buys,
|
||||
'sells': sells,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
getMarkets(): Markets {
|
||||
const allCurrencies = this.getCurrencies();
|
||||
const activeCurrencies = this.getCurrencies('active');
|
||||
const markets = {};
|
||||
|
||||
for (const currency of Object.keys(activeCurrencies)) {
|
||||
if (allCurrencies[currency].code === 'BTC') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isFiat = allCurrencies[currency]._type === 'fiat';
|
||||
const pmarketname = allCurrencies['BTC']['name'];
|
||||
|
||||
const lsymbol = isFiat ? 'BTC' : currency;
|
||||
const rsymbol = isFiat ? currency : 'BTC';
|
||||
const lname = isFiat ? pmarketname : allCurrencies[currency].name;
|
||||
const rname = isFiat ? allCurrencies[currency].name : pmarketname;
|
||||
const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type;
|
||||
const rtype = isFiat ? 'fiat' : 'crypto';
|
||||
const lprecision = 8;
|
||||
const rprecision = isFiat ? 2 : 8;
|
||||
const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase();
|
||||
|
||||
markets[pair] = {
|
||||
'pair': pair,
|
||||
'lname': lname,
|
||||
'rname': rname,
|
||||
'lsymbol': lsymbol,
|
||||
'rsymbol': rsymbol,
|
||||
'lprecision': lprecision,
|
||||
'rprecision': rprecision,
|
||||
'ltype': ltype,
|
||||
'rtype': rtype,
|
||||
'name': lname + '/' + rname,
|
||||
};
|
||||
}
|
||||
|
||||
return markets;
|
||||
}
|
||||
|
||||
getTrades(
|
||||
market: string,
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
trade_id_from?: string,
|
||||
trade_id_to?: string,
|
||||
direction?: 'buy' | 'sell',
|
||||
limit: number = 100,
|
||||
sort: 'asc' | 'desc' = 'desc',
|
||||
): BisqTrade[] {
|
||||
limit = Math.min(limit, 2000);
|
||||
const _market = market === 'all' ? undefined : market;
|
||||
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from,
|
||||
trade_id_to, trade_id_from, direction, sort, limit, false);
|
||||
|
||||
if (sort === 'asc') {
|
||||
matches.sort((a, b) => a.tradeDate - b.tradeDate);
|
||||
} else {
|
||||
matches.sort((a, b) => b.tradeDate - a.tradeDate);
|
||||
}
|
||||
|
||||
return matches.map((trade) => {
|
||||
const bsqTrade: BisqTrade = {
|
||||
direction: trade.primaryMarketDirection,
|
||||
price: trade._tradePriceStr,
|
||||
amount: trade._tradeAmountStr,
|
||||
volume: trade._tradeVolumeStr,
|
||||
payment_method: trade.paymentMethod,
|
||||
trade_id: trade.offerId,
|
||||
trade_date: trade.tradeDate,
|
||||
};
|
||||
if (market === 'all') {
|
||||
bsqTrade.market = trade._market;
|
||||
}
|
||||
return bsqTrade;
|
||||
});
|
||||
}
|
||||
|
||||
getVolumes(
|
||||
market?: string,
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
interval: Interval = 'auto',
|
||||
milliseconds?: boolean,
|
||||
timestamp: 'no' | 'yes' = 'yes',
|
||||
): MarketVolume[] {
|
||||
if (milliseconds) {
|
||||
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
|
||||
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
|
||||
}
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (interval === 'auto') {
|
||||
const range = timestamp_to - timestamp_from;
|
||||
interval = this.getIntervalFromRange(range);
|
||||
}
|
||||
|
||||
const intervals: any = {};
|
||||
const marketVolumes: MarketVolume[] = [];
|
||||
|
||||
for (const trade of trades) {
|
||||
const traded_at = trade['tradeDate'] / 1000;
|
||||
const interval_start = this.intervalStart(traded_at, interval);
|
||||
|
||||
if (!intervals[interval_start]) {
|
||||
intervals[interval_start] = {
|
||||
'volume': 0,
|
||||
'num_trades': 0,
|
||||
};
|
||||
}
|
||||
|
||||
const period = intervals[interval_start];
|
||||
period['period_start'] = interval_start;
|
||||
period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
|
||||
period['num_trades']++;
|
||||
}
|
||||
|
||||
for (const p in intervals) {
|
||||
if (intervals.hasOwnProperty(p)) {
|
||||
const period = intervals[p];
|
||||
marketVolumes.push({
|
||||
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
|
||||
num_trades: period['num_trades'],
|
||||
volume: this.intToBtc(period['volume']),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return marketVolumes;
|
||||
}
|
||||
|
||||
getTicker(
|
||||
market?: string,
|
||||
): Tickers | Ticker | null {
|
||||
if (market) {
|
||||
return this.getTickerFromMarket(market);
|
||||
}
|
||||
|
||||
if (this.tickersCache) {
|
||||
return this.tickersCache;
|
||||
}
|
||||
|
||||
const allMarkets = this.getMarkets();
|
||||
const tickers = {};
|
||||
for (const m in allMarkets) {
|
||||
if (allMarkets.hasOwnProperty(m)) {
|
||||
tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair);
|
||||
}
|
||||
}
|
||||
|
||||
return tickers;
|
||||
}
|
||||
|
||||
getTickerFromMarket(market: string): Ticker | null {
|
||||
let ticker: Ticker;
|
||||
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);
|
||||
|
||||
const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from));
|
||||
|
||||
const allCurrencies = this.getCurrencies();
|
||||
const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()];
|
||||
|
||||
if (periods[0]) {
|
||||
ticker = {
|
||||
'last': this.intToBtc(periods[0].close),
|
||||
'high': this.intToBtc(periods[0].high),
|
||||
'low': this.intToBtc(periods[0].low),
|
||||
'volume_left': this.intToBtc(periods[0].volume_left),
|
||||
'volume_right': this.intToBtc(periods[0].volume_right),
|
||||
'buy': null,
|
||||
'sell': null,
|
||||
};
|
||||
} else {
|
||||
const lastTrade = this.tradeDataByMarket[market];
|
||||
if (!lastTrade) {
|
||||
return null;
|
||||
}
|
||||
const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
|
||||
|
||||
const lastTradePrice = this.intToBtc(tradePrice);
|
||||
ticker = {
|
||||
'last': lastTradePrice,
|
||||
'high': lastTradePrice,
|
||||
'low': lastTradePrice,
|
||||
'volume_left': '0',
|
||||
'volume_right': '0',
|
||||
'buy': null,
|
||||
'sell': null,
|
||||
};
|
||||
}
|
||||
|
||||
const timestampFromMilli = timestamp_from * 1000;
|
||||
const timestampToMilli = timestamp_to * 1000;
|
||||
|
||||
const currencyPair = market.replace('_', '/').toUpperCase();
|
||||
const offersData = this.offersData.slice().sort((a, b) => a.price - b.price);
|
||||
|
||||
const buy = offersData.find((offer) => offer.currencyPair === currencyPair
|
||||
&& offer.primaryMarketDirection === 'BUY'
|
||||
&& offer.date >= timestampFromMilli
|
||||
&& offer.date <= timestampToMilli
|
||||
);
|
||||
const sell = offersData.find((offer) => offer.currencyPair === currencyPair
|
||||
&& offer.primaryMarketDirection === 'SELL'
|
||||
&& offer.date >= timestampFromMilli
|
||||
&& offer.date <= timestampToMilli
|
||||
);
|
||||
|
||||
if (buy) {
|
||||
ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
|
||||
}
|
||||
if (sell) {
|
||||
ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
|
||||
}
|
||||
|
||||
return ticker;
|
||||
}
|
||||
|
||||
getHloc(
|
||||
market: string,
|
||||
interval: Interval = 'auto',
|
||||
timestamp_from?: number,
|
||||
timestamp_to?: number,
|
||||
milliseconds?: boolean,
|
||||
timestamp: 'no' | 'yes' = 'yes',
|
||||
): HighLowOpenClose[] {
|
||||
if (milliseconds) {
|
||||
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
|
||||
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
|
||||
}
|
||||
if (!timestamp_from) {
|
||||
timestamp_from = new Date('2016-01-01').getTime() / 1000;
|
||||
}
|
||||
if (!timestamp_to) {
|
||||
timestamp_to = new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
|
||||
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
|
||||
|
||||
if (interval === 'auto') {
|
||||
const range = timestamp_to - timestamp_from;
|
||||
interval = this.getIntervalFromRange(range);
|
||||
}
|
||||
|
||||
const intervals = this.getTradesSummarized(trades, timestamp_from, interval);
|
||||
|
||||
const hloc: HighLowOpenClose[] = [];
|
||||
|
||||
for (const p in intervals) {
|
||||
if (intervals.hasOwnProperty(p)) {
|
||||
const period = intervals[p];
|
||||
hloc.push({
|
||||
period_start: timestamp === 'no' ? new Date(period['period_start'] * 1000).toISOString() : period['period_start'],
|
||||
open: this.intToBtc(period['open']),
|
||||
close: this.intToBtc(period['close']),
|
||||
high: this.intToBtc(period['high']),
|
||||
low: this.intToBtc(period['low']),
|
||||
avg: this.intToBtc(period['avg']),
|
||||
volume_right: this.intToBtc(period['volume_right']),
|
||||
volume_left: this.intToBtc(period['volume_left']),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hloc;
|
||||
}
|
||||
|
||||
private getIntervalFromRange(range: number): Interval {
|
||||
// two days range loads minute data
|
||||
if (range <= 3600) {
|
||||
// up to one hour range loads minutely data
|
||||
return 'minute';
|
||||
} else if (range <= 1 * 24 * 3600) {
|
||||
// up to one day range loads half-hourly data
|
||||
return 'half_hour';
|
||||
} else if (range <= 3 * 24 * 3600) {
|
||||
// up to 3 day range loads hourly data
|
||||
return 'hour';
|
||||
} else if (range <= 7 * 24 * 3600) {
|
||||
// up to 7 day range loads half-daily data
|
||||
return 'half_day';
|
||||
} else if (range <= 60 * 24 * 3600) {
|
||||
// up to 2 month range loads daily data
|
||||
return 'day';
|
||||
} else if (range <= 12 * 31 * 24 * 3600) {
|
||||
// up to one year range loads weekly data
|
||||
return 'week';
|
||||
} else if (range <= 12 * 31 * 24 * 3600) {
|
||||
// up to 5 year range loads monthly data
|
||||
return 'month';
|
||||
} else {
|
||||
// greater range loads yearly data
|
||||
return 'year';
|
||||
}
|
||||
}
|
||||
|
||||
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
|
||||
const intervals: any = {};
|
||||
const intervals_prices: any = {};
|
||||
|
||||
for (const trade of trades) {
|
||||
const traded_at = trade.tradeDate / 1000;
|
||||
const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval);
|
||||
|
||||
if (!intervals[interval_start]) {
|
||||
intervals[interval_start] = {
|
||||
'open': 0,
|
||||
'close': 0,
|
||||
'high': 0,
|
||||
'low': 0,
|
||||
'avg': 0,
|
||||
'volume_right': 0,
|
||||
'volume_left': 0,
|
||||
};
|
||||
intervals_prices[interval_start] = [];
|
||||
}
|
||||
const period = intervals[interval_start];
|
||||
const price = trade._tradePrice;
|
||||
|
||||
if (!intervals_prices[interval_start]['leftvol']) {
|
||||
intervals_prices[interval_start]['leftvol'] = [];
|
||||
}
|
||||
if (!intervals_prices[interval_start]['rightvol']) {
|
||||
intervals_prices[interval_start]['rightvol'] = [];
|
||||
}
|
||||
|
||||
intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount);
|
||||
intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume);
|
||||
|
||||
if (price) {
|
||||
const plow = period['low'];
|
||||
period['period_start'] = interval_start;
|
||||
period['open'] = period['open'] || price;
|
||||
period['close'] = price;
|
||||
period['high'] = price > period['high'] ? price : period['high'];
|
||||
period['low'] = (plow && price > plow) ? period['low'] : price;
|
||||
period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0)
|
||||
/ intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000;
|
||||
period['volume_left'] += trade._tradeAmount;
|
||||
period['volume_right'] += trade._tradeVolume;
|
||||
}
|
||||
}
|
||||
return intervals;
|
||||
}
|
||||
|
||||
private getTradesByCriteria(
|
||||
market: string | undefined,
|
||||
timestamp_to: number,
|
||||
timestamp_from: number,
|
||||
trade_id_to: string | undefined,
|
||||
trade_id_from: string | undefined,
|
||||
direction: 'buy' | 'sell' | undefined,
|
||||
sort: string,
|
||||
limit: number,
|
||||
integerAmounts: boolean = true,
|
||||
): TradesData[] {
|
||||
let trade_id_from_ts: number | null = null;
|
||||
let trade_id_to_ts: number | null = null;
|
||||
const allCurrencies = this.getCurrencies();
|
||||
|
||||
const timestampFromMilli = timestamp_from * 1000;
|
||||
const timestampToMilli = timestamp_to * 1000;
|
||||
|
||||
// note: the offer_id_from/to depends on iterating over trades in
|
||||
// descending chronological order.
|
||||
const tradesDataSorted = this.tradesData.slice();
|
||||
if (sort === 'asc') {
|
||||
tradesDataSorted.reverse();
|
||||
}
|
||||
|
||||
let matches: TradesData[] = [];
|
||||
for (const trade of tradesDataSorted) {
|
||||
if (trade_id_from === trade.offerId) {
|
||||
trade_id_from_ts = trade.tradeDate;
|
||||
}
|
||||
if (trade_id_to === trade.offerId) {
|
||||
trade_id_to_ts = trade.tradeDate;
|
||||
}
|
||||
if (trade_id_to && trade_id_to_ts === null) {
|
||||
continue;
|
||||
}
|
||||
if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (market && market !== trade._market) {
|
||||
continue;
|
||||
}
|
||||
if (timestampFromMilli && timestampFromMilli > trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (timestampToMilli && timestampToMilli < trade.tradeDate) {
|
||||
continue;
|
||||
}
|
||||
if (direction && direction !== trade.direction.toLowerCase()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out bogus trades with BTC/BTC or XXX/XXX market.
|
||||
// See github issue: https://github.com/bitsquare/bitsquare/issues/883
|
||||
const currencyPairs = trade.currencyPair.split('/');
|
||||
if (currencyPairs[0] === currencyPairs[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currencyLeft = allCurrencies[currencyPairs[0]];
|
||||
const currencyRight = allCurrencies[currencyPairs[1]];
|
||||
|
||||
if (!currencyLeft || !currencyRight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
|
||||
const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision);
|
||||
const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision);
|
||||
|
||||
if (integerAmounts) {
|
||||
trade._tradePrice = tradePrice;
|
||||
trade._tradeAmount = tradeAmount;
|
||||
trade._tradeVolume = tradeVolume;
|
||||
trade._offerAmount = trade.offerAmount;
|
||||
} else {
|
||||
trade._tradePriceStr = this.intToBtc(tradePrice);
|
||||
trade._tradeAmountStr = this.intToBtc(tradeAmount);
|
||||
trade._tradeVolumeStr = this.intToBtc(tradeVolume);
|
||||
trade._offerAmountStr = this.intToBtc(trade.offerAmount);
|
||||
}
|
||||
|
||||
matches.push(trade);
|
||||
|
||||
if (matches.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) {
|
||||
matches = [];
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private intervalStart(ts: number, interval: string): number {
|
||||
switch (interval) {
|
||||
case 'minute':
|
||||
return (ts - (ts % 60));
|
||||
case '10_minute':
|
||||
return (ts - (ts % 600));
|
||||
case 'half_hour':
|
||||
return (ts - (ts % 1800));
|
||||
case 'hour':
|
||||
return (ts - (ts % 3600));
|
||||
case 'half_day':
|
||||
return (ts - (ts % (3600 * 12)));
|
||||
case 'day':
|
||||
return datetime.strtotime('midnight today', ts);
|
||||
case 'week':
|
||||
return datetime.strtotime('midnight sunday last week', ts);
|
||||
case 'month':
|
||||
return datetime.strtotime('midnight first day of this month', ts);
|
||||
case 'year':
|
||||
return datetime.strtotime('midnight first day of january', ts);
|
||||
default:
|
||||
throw new Error('Unsupported interval: ' + interval);
|
||||
}
|
||||
}
|
||||
|
||||
private offerDataToOffer(offer: OffersData, market: string): Offer {
|
||||
const currencyPairs = market.split('_');
|
||||
const currencyRight = this.allCurrenciesIndexed[currencyPairs[1].toUpperCase()];
|
||||
const currencyLeft = this.allCurrenciesIndexed[currencyPairs[0].toUpperCase()];
|
||||
const price = offer['primaryMarketPrice'] * Math.pow( 10, 8 - currencyRight['precision']);
|
||||
const amount = offer['primaryMarketAmount'] * Math.pow( 10, 8 - currencyLeft['precision']);
|
||||
const volume = offer['primaryMarketVolume'] * Math.pow( 10, 8 - currencyRight['precision']);
|
||||
|
||||
return {
|
||||
offer_id: offer.id,
|
||||
offer_date: offer.date,
|
||||
direction: offer.primaryMarketDirection,
|
||||
min_amount: this.intToBtc(offer.minAmount),
|
||||
amount: this.intToBtc(amount),
|
||||
price: this.intToBtc(price),
|
||||
volume: this.intToBtc(volume),
|
||||
payment_method: offer.paymentMethod,
|
||||
offer_fee_txid: null,
|
||||
};
|
||||
}
|
||||
|
||||
private intToBtc(val: number): string {
|
||||
return (val / 100000000).toFixed(8);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BisqMarketsApi();
|
||||
@@ -1,131 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as fs from 'fs';
|
||||
import { OffersData as OffersData, TradesData, Currency } from './interfaces';
|
||||
import bisqMarket from './markets-api';
|
||||
import logger from '../../logger';
|
||||
|
||||
class Bisq {
|
||||
private static FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE = 4000;
|
||||
private static MARKET_JSON_PATH = config.BISQ_MARKETS.DATA_PATH;
|
||||
private static MARKET_JSON_FILE_PATHS = {
|
||||
activeCryptoCurrency: '/active_crypto_currency_list.json',
|
||||
activeFiatCurrency: '/active_fiat_currency_list.json',
|
||||
cryptoCurrency: '/crypto_currency_list.json',
|
||||
fiatCurrency: '/fiat_currency_list.json',
|
||||
offers: '/offers_statistics.json',
|
||||
trades: '/trade_statistics.json',
|
||||
};
|
||||
|
||||
private cryptoCurrencyLastMtime = new Date('2016-01-01');
|
||||
private fiatCurrencyLastMtime = new Date('2016-01-01');
|
||||
private offersLastMtime = new Date('2016-01-01');
|
||||
private tradesLastMtime = new Date('2016-01-01');
|
||||
|
||||
private subdirectoryWatcher: fs.FSWatcher | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
startBisqService(): void {
|
||||
this.checkForBisqDataFolder();
|
||||
this.loadBisqDumpFile();
|
||||
this.startBisqDirectoryWatcher();
|
||||
}
|
||||
|
||||
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.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private startBisqDirectoryWatcher() {
|
||||
if (this.subdirectoryWatcher) {
|
||||
this.subdirectoryWatcher.close();
|
||||
}
|
||||
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
|
||||
logger.warn(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Trying to restart sub directory watcher again in 3 minutes.`);
|
||||
setTimeout(() => this.startBisqDirectoryWatcher(), 180000);
|
||||
return;
|
||||
}
|
||||
let fsWait: NodeJS.Timeout | null = null;
|
||||
this.subdirectoryWatcher = fs.watch(Bisq.MARKET_JSON_PATH, () => {
|
||||
if (fsWait) {
|
||||
clearTimeout(fsWait);
|
||||
}
|
||||
fsWait = setTimeout(() => {
|
||||
logger.debug(`Change detected in the Bisq market data folder.`);
|
||||
this.loadBisqDumpFile();
|
||||
}, Bisq.FOLDER_WATCH_CHANGE_DETECTION_DEBOUNCE);
|
||||
});
|
||||
}
|
||||
|
||||
private async loadBisqDumpFile(): Promise<void> {
|
||||
const start = new Date().getTime();
|
||||
try {
|
||||
let marketsDataUpdated = false;
|
||||
const cryptoMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
|
||||
const fiatMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
|
||||
if (cryptoMtime > this.cryptoCurrencyLastMtime || fiatMtime > this.fiatCurrencyLastMtime) {
|
||||
const cryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency);
|
||||
const fiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.fiatCurrency);
|
||||
const activeCryptoCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeCryptoCurrency);
|
||||
const activeFiatCurrencyData = await this.loadData<Currency[]>(Bisq.MARKET_JSON_FILE_PATHS.activeFiatCurrency);
|
||||
logger.debug('Updating Bisq Market Currency Data');
|
||||
bisqMarket.setCurrencyData(cryptoCurrencyData, fiatCurrencyData, activeCryptoCurrencyData, activeFiatCurrencyData);
|
||||
if (cryptoMtime > this.cryptoCurrencyLastMtime) {
|
||||
this.cryptoCurrencyLastMtime = cryptoMtime;
|
||||
}
|
||||
if (fiatMtime > this.fiatCurrencyLastMtime) {
|
||||
this.fiatCurrencyLastMtime = fiatMtime;
|
||||
}
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
const offersMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.offers);
|
||||
if (offersMtime > this.offersLastMtime) {
|
||||
const offersData = await this.loadData<OffersData[]>(Bisq.MARKET_JSON_FILE_PATHS.offers);
|
||||
logger.debug('Updating Bisq Market Offers Data');
|
||||
bisqMarket.setOffersData(offersData);
|
||||
this.offersLastMtime = offersMtime;
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
const tradesMtime = this.getFileMtime(Bisq.MARKET_JSON_FILE_PATHS.trades);
|
||||
if (tradesMtime > this.tradesLastMtime) {
|
||||
const tradesData = await this.loadData<TradesData[]>(Bisq.MARKET_JSON_FILE_PATHS.trades);
|
||||
logger.debug('Updating Bisq Market Trades Data');
|
||||
bisqMarket.setTradesData(tradesData);
|
||||
this.tradesLastMtime = tradesMtime;
|
||||
marketsDataUpdated = true;
|
||||
}
|
||||
if (marketsDataUpdated) {
|
||||
bisqMarket.updateCache();
|
||||
const time = new Date().getTime() - start;
|
||||
logger.debug('Bisq market data updated in ' + time + ' ms');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('loadBisqMarketDataDumpFile() error.' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private getFileMtime(path: string): Date {
|
||||
const stats = fs.statSync(Bisq.MARKET_JSON_PATH + path);
|
||||
return stats.mtime;
|
||||
}
|
||||
|
||||
private loadData<T>(path: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(Bisq.MARKET_JSON_PATH + path, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
resolve(parsedData);
|
||||
} catch (e) {
|
||||
reject('JSON parse error (' + path + ')');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Bisq();
|
||||
@@ -1,14 +1,19 @@
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import { IMempoolInfo, ITransaction, IBlock } from '../../interfaces';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
|
||||
$getBlockHeightTip(): Promise<number>;
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||
$getBlockHash(height: number): Promise<string>;
|
||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$getAddressPrefix(prefix: string): string[];
|
||||
getMempoolInfo(): Promise<IMempoolInfo>;
|
||||
getRawMempool(): Promise<ITransaction['txid'][]>;
|
||||
getRawTransaction(txId: string): Promise<ITransaction>;
|
||||
getBlockCount(): Promise<number>;
|
||||
getBlockAndTransactions(hash: string): Promise<IBlock>;
|
||||
getBlockHash(height: number): Promise<string>;
|
||||
|
||||
getBlock(hash: string): Promise<IBlock>;
|
||||
getBlockTransactions(hash: string): Promise<IBlock>;
|
||||
getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock>;
|
||||
getBlocks(): Promise<string>;
|
||||
getBlocksFromHeight(height: number): Promise<string>;
|
||||
getAddress(address: string): Promise<IBlock>;
|
||||
getAddressTransactions(address: string): Promise<IBlock>;
|
||||
getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock>;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import config from '../../config';
|
||||
const config = require('../../../mempool-config.json');
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import EsploraApi from './esplora-api';
|
||||
import BitcoinApi from './bitcoin-api';
|
||||
import ElectrumApi from './electrum-api';
|
||||
import BitcoindApi from './bitcoind-api';
|
||||
import ElectrsApi from './electrs-api';
|
||||
|
||||
function bitcoinApiFactory(): AbstractBitcoinApi {
|
||||
switch (config.MEMPOOL.BACKEND) {
|
||||
case 'esplora':
|
||||
return new EsploraApi();
|
||||
case 'electrum':
|
||||
return new ElectrumApi();
|
||||
case 'none':
|
||||
function factory(): AbstractBitcoinApi {
|
||||
switch (config.BACKEND_API) {
|
||||
case 'electrs':
|
||||
return new ElectrsApi();
|
||||
case 'bitcoind':
|
||||
default:
|
||||
return new BitcoinApi();
|
||||
return new BitcoindApi();
|
||||
}
|
||||
}
|
||||
|
||||
export default bitcoinApiFactory();
|
||||
export default factory();
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
export namespace IBitcoinApi {
|
||||
export interface MempoolInfo {
|
||||
loaded: boolean; // (boolean) True if the mempool is fully loaded
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
export interface RawMempool { [txId: string]: MempoolEntry; }
|
||||
|
||||
export interface MempoolEntry {
|
||||
vsize: number; // (numeric) virtual transaction size as defined in BIP 141.
|
||||
weight: number; // (numeric) transaction weight as defined in BIP 141.
|
||||
time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
|
||||
height: number; // (numeric) block height when transaction entered pool
|
||||
descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one)
|
||||
descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one)
|
||||
ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one)
|
||||
ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one)
|
||||
wtxid: string; // (string) hash of serialized transactionumber; including witness data
|
||||
fees: {
|
||||
base: number; // (numeric) transaction fee in BTC
|
||||
modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC
|
||||
ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
|
||||
descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
|
||||
};
|
||||
depends: string[]; // (string) parent transaction id
|
||||
spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction
|
||||
'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
hash: string; // (string) the block hash (same as provided)
|
||||
confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain
|
||||
size: number; // (numeric) The block size
|
||||
strippedsize: number; // (numeric) The block size excluding witness data
|
||||
weight: number; // (numeric) The block weight as defined in BIP 141
|
||||
height: number; // (numeric) The block height or index
|
||||
version: number; // (numeric) The block version
|
||||
versionHex: string; // (string) The block version formatted in hexadecimal
|
||||
merkleroot: string; // (string) The merkle root
|
||||
tx: Transaction[];
|
||||
time: number; // (numeric) The block time expressed in UNIX epoch time
|
||||
mediantime: number; // (numeric) The median block time expressed in UNIX epoch time
|
||||
nonce: number; // (numeric) The nonce
|
||||
bits: string; // (string) The bits
|
||||
difficulty: number; // (numeric) The difficulty
|
||||
chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex)
|
||||
nTx: number; // (numeric) The number of transactions in the block
|
||||
previousblockhash: string; // (string) The hash of the previous block
|
||||
nextblockhash: string; // (string) The hash of the next block
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not
|
||||
hex: string; // (string) The serialized, hex-encoded data for 'txid'
|
||||
txid: string; // (string) The transaction id (same as provided)
|
||||
hash: string; // (string) The transaction hash (differs from txid for witness transactions)
|
||||
size: number; // (numeric) The serialized transaction size
|
||||
vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions)
|
||||
weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
|
||||
version: number; // (numeric) The version
|
||||
locktime: number; // (numeric) The lock time
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
blockhash: string; // (string) the block hash
|
||||
confirmations: number; // (numeric) The confirmations
|
||||
blocktime: number; // (numeric) The block time expressed in UNIX epoch time
|
||||
time: number; // (numeric) Same as blocktime
|
||||
}
|
||||
|
||||
interface Vin {
|
||||
txid?: string; // (string) The transaction id
|
||||
vout?: number; // (string)
|
||||
scriptSig?: { // (json object) The script
|
||||
asm: string; // (string) asm
|
||||
hex: string; // (string) hex
|
||||
};
|
||||
sequence: number; // (numeric) The script sequence number
|
||||
txinwitness?: string[]; // (string) hex-encoded witness data
|
||||
coinbase?: string;
|
||||
}
|
||||
|
||||
interface Vout {
|
||||
value: number; // (numeric) The value in BTC
|
||||
n: number; // (numeric) index
|
||||
scriptPubKey: { // (json object)
|
||||
asm: string; // (string) the asm
|
||||
hex: string; // (string) the hex
|
||||
reqSigs: number; // (numeric) The required sigs
|
||||
type: string; // (string) The type, eg 'pubkeyhash'
|
||||
addresses: string[] // (string) bitcoin address
|
||||
};
|
||||
}
|
||||
|
||||
export interface AddressInformation {
|
||||
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
||||
address: string; // (string) The bitcoin address validated
|
||||
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
||||
isscript: boolean; // (boolean) If the key is a script
|
||||
iswitness: boolean; // (boolean) If the address is a witness
|
||||
witness_version?: boolean; // (numeric, optional) The version number of the witness program
|
||||
witness_program: string; // (string, optional) The hex value of the witness program
|
||||
}
|
||||
|
||||
export interface ChainTips {
|
||||
height: number; // (numeric) height of the chain tip
|
||||
hash: string; // (string) block hash of the tip
|
||||
branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
|
||||
status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as bitcoin from '@mempool/bitcoin';
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import blocks from '../blocks';
|
||||
import mempool from '../mempool';
|
||||
import { TransactionExtended } from '../../mempool.interfaces';
|
||||
|
||||
class BitcoinApi implements AbstractBitcoinApi {
|
||||
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
|
||||
private bitcoindClient: any;
|
||||
|
||||
constructor() {
|
||||
this.bitcoindClient = new bitcoin.Client({
|
||||
host: config.CORE_RPC.HOST,
|
||||
port: config.CORE_RPC.PORT,
|
||||
user: config.CORE_RPC.USERNAME,
|
||||
pass: config.CORE_RPC.PASSWORD,
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
$getRawTransactionBitcoind(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
|
||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
||||
.then((transaction: IBitcoinApi.Transaction) => {
|
||||
if (skipConversion) {
|
||||
return transaction;
|
||||
}
|
||||
return this.$convertTransaction(transaction, addPrevout);
|
||||
});
|
||||
}
|
||||
|
||||
$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) {
|
||||
return transaction;
|
||||
}
|
||||
return this.$convertTransaction(transaction, addPrevout);
|
||||
});
|
||||
}
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
return this.bitcoindClient.getChainTips()
|
||||
.then((result: IBitcoinApi.ChainTips[]) => result[0].height);
|
||||
}
|
||||
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||
return this.bitcoindClient.getBlock(hash, 1)
|
||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||
}
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
return this.bitcoindClient.getBlockHash(height);
|
||||
}
|
||||
|
||||
async $getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||
const foundBlock = blocks.getBlocks().find((block) => block.id === hash);
|
||||
if (foundBlock) {
|
||||
return foundBlock;
|
||||
}
|
||||
|
||||
return this.bitcoindClient.getBlock(hash)
|
||||
.then((block: IBitcoinApi.Block) => this.convertBlock(block));
|
||||
}
|
||||
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||
throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
|
||||
}
|
||||
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||
return this.bitcoindClient.getRawMemPool();
|
||||
}
|
||||
|
||||
$getAddressPrefix(prefix: 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.push(vout.scriptpubkey_address);
|
||||
if (found.length >= 10) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
|
||||
let esploraTransaction: IEsploraApi.Transaction = {
|
||||
txid: transaction.txid,
|
||||
version: transaction.version,
|
||||
locktime: transaction.locktime,
|
||||
size: transaction.size,
|
||||
weight: transaction.weight,
|
||||
fee: 0,
|
||||
vin: [],
|
||||
vout: [],
|
||||
status: { confirmed: false },
|
||||
};
|
||||
|
||||
esploraTransaction.vout = transaction.vout.map((vout) => {
|
||||
return {
|
||||
value: vout.value * 100000000,
|
||||
scriptpubkey: vout.scriptPubKey.hex,
|
||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
||||
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.asm) : '',
|
||||
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
||||
};
|
||||
});
|
||||
|
||||
esploraTransaction.vin = transaction.vin.map((vin) => {
|
||||
return {
|
||||
is_coinbase: !!vin.coinbase,
|
||||
prevout: null,
|
||||
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
|
||||
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.asm) || '',
|
||||
sequence: vin.sequence,
|
||||
txid: vin.txid || '',
|
||||
vout: vin.vout || 0,
|
||||
witness: vin.txinwitness,
|
||||
};
|
||||
});
|
||||
|
||||
if (transaction.confirmations) {
|
||||
esploraTransaction.status = {
|
||||
confirmed: true,
|
||||
block_height: blocks.getCurrentBlockHeight() - transaction.confirmations + 1,
|
||||
block_hash: transaction.blockhash,
|
||||
block_time: transaction.blocktime,
|
||||
};
|
||||
}
|
||||
|
||||
if (transaction.confirmations) {
|
||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
|
||||
} else {
|
||||
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
||||
}
|
||||
|
||||
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',
|
||||
'pubkeyhash': 'p2pkh',
|
||||
'scripthash': 'p2sh',
|
||||
'witness_v0_keyhash': 'v0_p2wpkh',
|
||||
'witness_v0_scripthash': 'v0_p2wsh',
|
||||
'witness_v1_taproot': 'v1_p2tr',
|
||||
'nonstandard': 'nonstandard',
|
||||
'nulldata': 'op_return'
|
||||
};
|
||||
|
||||
if (map[outputType]) {
|
||||
return map[outputType];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
|
||||
if (transaction.fee) {
|
||||
return transaction;
|
||||
}
|
||||
let mempoolEntry: IBitcoinApi.MempoolEntry;
|
||||
if (!mempool.isInSync() && !this.rawMempoolCache) {
|
||||
this.rawMempoolCache = await this.$getRawMempoolVerbose();
|
||||
}
|
||||
if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
|
||||
mempoolEntry = this.rawMempoolCache[transaction.txid];
|
||||
} else {
|
||||
mempoolEntry = await this.$getMempoolEntry(transaction.txid);
|
||||
}
|
||||
transaction.fee = mempoolEntry.fees.base * 100000000;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
protected async $addPrevouts(transaction: TransactionExtended): Promise<TransactionExtended> {
|
||||
for (const vin of transaction.vin) {
|
||||
if (vin.prevout) {
|
||||
continue;
|
||||
}
|
||||
const innerTx = await this.$getRawTransaction(vin.txid, false);
|
||||
vin.prevout = innerTx.vout[vin.vout];
|
||||
this.addInnerScriptsToVin(vin);
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
protected $returnCoinbaseTransaction(): Promise<IEsploraApi.Transaction> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
protected $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
|
||||
return this.bitcoindClient.validateAddress(address);
|
||||
}
|
||||
|
||||
private $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
|
||||
return this.bitcoindClient.getMempoolEntry(txid);
|
||||
}
|
||||
|
||||
private $getRawMempoolVerbose(): Promise<IBitcoinApi.RawMempool> {
|
||||
return this.bitcoindClient.getRawMemPool(true);
|
||||
}
|
||||
|
||||
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 (const vin of transaction.vin) {
|
||||
const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
|
||||
if (addPrevout) {
|
||||
vin.prevout = innerTx.vout[vin.vout];
|
||||
this.addInnerScriptsToVin(vin);
|
||||
}
|
||||
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(str: string): string {
|
||||
const a = str.split(' ');
|
||||
const b: string[] = [];
|
||||
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 {
|
||||
chunk = chunk.replace('[ALL]', '01');
|
||||
if (chunk === '0') {
|
||||
b.push('OP_0');
|
||||
} else {
|
||||
b.push('OP_PUSHBYTES_' + Math.round(chunk.length / 2) + ' ' + chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
return b.join(' ');
|
||||
}
|
||||
|
||||
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
||||
if (!vin.prevout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
||||
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(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(bitcoinjs.script.toASM(Buffer.from(witnessScript, 'hex')));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BitcoinApi;
|
||||
@@ -1,45 +0,0 @@
|
||||
import config from '../../config';
|
||||
import * as bitcoin from '@mempool/bitcoin';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
|
||||
class BitcoinBaseApi {
|
||||
bitcoindClient: any;
|
||||
bitcoindClientMempoolInfo: any;
|
||||
|
||||
constructor() {
|
||||
this.bitcoindClient = new bitcoin.Client({
|
||||
host: config.CORE_RPC.HOST,
|
||||
port: config.CORE_RPC.PORT,
|
||||
user: config.CORE_RPC.USERNAME,
|
||||
pass: config.CORE_RPC.PASSWORD,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
||||
this.bitcoindClientMempoolInfo = new bitcoin.Client({
|
||||
host: config.CORE_RPC_MINFEE.HOST,
|
||||
port: config.CORE_RPC_MINFEE.PORT,
|
||||
user: config.CORE_RPC_MINFEE.USERNAME,
|
||||
pass: config.CORE_RPC_MINFEE.PASSWORD,
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$getMempoolInfo(): Promise<IBitcoinApi.MempoolInfo> {
|
||||
if (config.CORE_RPC_MINFEE.ENABLED) {
|
||||
return Promise.all([
|
||||
this.bitcoindClient.getMempoolInfo(),
|
||||
this.bitcoindClientMempoolInfo.getMempoolInfo()
|
||||
]).then(([mempoolInfo, secondMempoolInfo]) => {
|
||||
mempoolInfo.maxmempool = secondMempoolInfo.maxmempool;
|
||||
mempoolInfo.mempoolminfee = secondMempoolInfo.mempoolminfee;
|
||||
mempoolInfo.minrelaytxfee = secondMempoolInfo.minrelaytxfee;
|
||||
return mempoolInfo;
|
||||
});
|
||||
}
|
||||
return this.bitcoindClient.getMempoolInfo();
|
||||
}
|
||||
}
|
||||
|
||||
export default new BitcoinBaseApi();
|
||||
110
backend/src/api/bitcoin/bitcoind-api.ts
Normal file
110
backend/src/api/bitcoin/bitcoind-api.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
const config = require('../../../mempool-config.json');
|
||||
import * as bitcoin from 'bitcoin';
|
||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
|
||||
class BitcoindApi implements AbstractBitcoinApi {
|
||||
client: any;
|
||||
|
||||
constructor() {
|
||||
this.client = new bitcoin.Client({
|
||||
host: config.BITCOIN_NODE_HOST,
|
||||
port: config.BITCOIN_NODE_PORT,
|
||||
user: config.BITCOIN_NODE_USER,
|
||||
pass: config.BITCOIN_NODE_PASS,
|
||||
});
|
||||
}
|
||||
|
||||
getMempoolInfo(): Promise<IMempoolInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getMempoolInfo((err: Error, mempoolInfo: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(mempoolInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRawMempool(): Promise<ITransaction['txid'][]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getRawMemPool((err: Error, transactions: ITransaction['txid'][]) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(transactions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRawTransaction(txId: string): Promise<ITransaction> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getRawTransaction(txId, true, (err: Error, txData: ITransaction) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(txData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockCount(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getBlockCount((err: Error, response: number) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockAndTransactions(hash: string, verbosity: 1 | 2 = 1): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getBlock(hash, verbosity, (err: Error, block: IBlock) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(block);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockHash(height: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.getBlockHash(height, (err: Error, response: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlock(hash: string): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getBlocks(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getBlocksFromHeight(height: number): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getBlockTransactions(hash: string): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getAddress(address: string): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getAddressTransactions(address: string): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export default BitcoindApi;
|
||||
229
backend/src/api/bitcoin/electrs-api.ts
Normal file
229
backend/src/api/bitcoin/electrs-api.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
const config = require('../../../mempool-config.json');
|
||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import * as request from 'request';
|
||||
|
||||
class ElectrsApi implements AbstractBitcoinApi {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
getMempoolInfo(): Promise<IMempoolInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/mempool', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve({
|
||||
size: response.count,
|
||||
bytes: response.vsize,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRawMempool(): Promise<ITransaction['txid'][]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getRawTransaction(txId: string): Promise<ITransaction> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/tx/' + txId, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
response.vsize = Math.round(response.weight / 4);
|
||||
response.fee = response.fee / 100000000;
|
||||
response.blockhash = response.status.block_hash;
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockCount(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/blocks/tip/height', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockAndTransactions(hash: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
request(config.ELECTRS_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err2, res2, response2) => {
|
||||
if (err2) {
|
||||
reject(err2);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
const block = response;
|
||||
block.hash = hash;
|
||||
block.nTx = block.tx_count;
|
||||
block.time = block.timestamp;
|
||||
block.tx = response2;
|
||||
|
||||
resolve(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockHash(height: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/block-height/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlocks(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/blocks', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlocksFromHeight(height: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/blocks/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlock(hash: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockTransactions(hash: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/block/' + hash + '/txs', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/block/' + hash + '/txs/' + index, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAddress(address: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/address/' + address, { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAddressTransactions(address: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/address/' + address + '/txs', { json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(config.ELECTRS_API_URL + '/address/' + address + '/txs/chain/' + lastSeenTxid,
|
||||
{ json: true, timeout: 10000 }, (err, res, response) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
reject(response);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ElectrsApi;
|
||||
@@ -1,12 +0,0 @@
|
||||
export namespace IElectrumApi {
|
||||
export interface ScriptHashBalance {
|
||||
confirmed: number;
|
||||
unconfirmed: number;
|
||||
}
|
||||
|
||||
export interface ScriptHashHistory {
|
||||
height: number;
|
||||
tx_hash: string;
|
||||
fee?: number;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import config from '../../config';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import { IElectrumApi } from './electrum-api.interface';
|
||||
import BitcoinApi from './bitcoin-api';
|
||||
import mempool from '../mempool';
|
||||
import logger from '../../logger';
|
||||
import * as ElectrumClient from '@mempool/electrum-client';
|
||||
import * as sha256 from 'crypto-js/sha256';
|
||||
import * as hexEnc from 'crypto-js/enc-hex';
|
||||
import loadingIndicators from '../loading-indicators';
|
||||
import memoryCache from '../memory-cache';
|
||||
|
||||
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
||||
private electrumClient: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
|
||||
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };
|
||||
|
||||
const electrumCallbacks = {
|
||||
onConnect: (client, versionInfo) => { logger.info(`Connected to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT} (${JSON.stringify(versionInfo)})`); },
|
||||
onClose: (client) => { logger.info(`Disconnected from Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`); },
|
||||
onError: (err) => { logger.err(`Electrum error: ${JSON.stringify(err)}`); },
|
||||
onLog: (str) => { logger.debug(str); },
|
||||
};
|
||||
|
||||
this.electrumClient = new ElectrumClient(
|
||||
config.ELECTRUM.PORT,
|
||||
config.ELECTRUM.HOST,
|
||||
config.ELECTRUM.TLS_ENABLED ? 'tls' : 'tcp',
|
||||
null,
|
||||
electrumCallbacks
|
||||
);
|
||||
|
||||
this.electrumClient.initElectrum(electrumConfig, electrumPersistencePolicy)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
logger.err(`Error connecting to Electrum Server at ${config.ELECTRUM.HOST}:${config.ELECTRUM.PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
async $getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||
const addressInfo = await this.$validateAddress(address);
|
||||
if (!addressInfo || !addressInfo.isvalid) {
|
||||
return ({
|
||||
'address': address,
|
||||
'chain_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': 0,
|
||||
'tx_count': 0
|
||||
},
|
||||
'mempool_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': 0,
|
||||
'tx_count': 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const balance = await this.$getScriptHashBalance(addressInfo.scriptPubKey);
|
||||
const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
|
||||
|
||||
const unconfirmed = history.filter((h) => h.fee).length;
|
||||
|
||||
return {
|
||||
'address': addressInfo.address,
|
||||
'chain_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
|
||||
'tx_count': history.length - unconfirmed,
|
||||
},
|
||||
'mempool_stats': {
|
||||
'funded_txo_count': 0,
|
||||
'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
|
||||
'spent_txo_count': 0,
|
||||
'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
|
||||
'tx_count': unconfirmed,
|
||||
},
|
||||
'electrum': true,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e === 'failed to get confirmed status') {
|
||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||
}
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||
const addressInfo = await this.$validateAddress(address);
|
||||
if (!addressInfo || !addressInfo.isvalid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
loadingIndicators.setProgress('address-' + address, 0);
|
||||
|
||||
const transactions: IEsploraApi.Transaction[] = [];
|
||||
const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
|
||||
history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999));
|
||||
|
||||
let startingIndex = 0;
|
||||
if (lastSeenTxId) {
|
||||
const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId);
|
||||
if (pos) {
|
||||
startingIndex = pos + 1;
|
||||
}
|
||||
}
|
||||
const endIndex = Math.min(startingIndex + 10, history.length);
|
||||
|
||||
for (let i = startingIndex; i < endIndex; i++) {
|
||||
const tx = await this.$getRawTransaction(history[i].tx_hash, false, true);
|
||||
transactions.push(tx);
|
||||
loadingIndicators.setProgress('address-' + address, (i + 1) / endIndex * 100);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
} catch (e) {
|
||||
loadingIndicators.setProgress('address-' + address, 100);
|
||||
if (e === 'failed to get confirmed status') {
|
||||
e = 'The number of transactions on this address exceeds the Electrum server limit';
|
||||
}
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
|
||||
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
||||
}
|
||||
|
||||
private $getScriptHashHistory(scriptHash: string): Promise<IElectrumApi.ScriptHashHistory[]> {
|
||||
const fromCache = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scriptHash);
|
||||
if (fromCache) {
|
||||
return Promise.resolve(fromCache);
|
||||
}
|
||||
return this.electrumClient.blockchainScripthash_getHistory(this.encodeScriptHash(scriptHash))
|
||||
.then((history) => {
|
||||
memoryCache.set('Scripthash_getHistory', scriptHash, history, 2);
|
||||
return history;
|
||||
});
|
||||
}
|
||||
|
||||
private encodeScriptHash(scriptPubKey: string): string {
|
||||
const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
|
||||
return addrScripthash.match(/.{2}/g).reverse().join('');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BitcoindElectrsApi;
|
||||
@@ -1,169 +0,0 @@
|
||||
export namespace IEsploraApi {
|
||||
export interface Transaction {
|
||||
txid: string;
|
||||
version: number;
|
||||
locktime: number;
|
||||
size: number;
|
||||
weight: number;
|
||||
fee: number;
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface Recent {
|
||||
txid: string;
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface Vin {
|
||||
txid: string;
|
||||
vout: number;
|
||||
is_coinbase: boolean;
|
||||
scriptsig: string;
|
||||
scriptsig_asm: string;
|
||||
inner_redeemscript_asm?: string;
|
||||
inner_witnessscript_asm?: string;
|
||||
sequence: any;
|
||||
witness?: string[];
|
||||
prevout: Vout | null;
|
||||
// Elements
|
||||
is_pegin?: boolean;
|
||||
issuance?: Issuance;
|
||||
}
|
||||
|
||||
interface Issuance {
|
||||
asset_id: string;
|
||||
is_reissuance: string;
|
||||
asset_blinding_nonce: string;
|
||||
asset_entropy: string;
|
||||
contract_hash: string;
|
||||
assetamount?: number;
|
||||
assetamountcommitment?: string;
|
||||
tokenamount?: number;
|
||||
tokenamountcommitment?: string;
|
||||
}
|
||||
|
||||
export interface Vout {
|
||||
scriptpubkey: string;
|
||||
scriptpubkey_asm: string;
|
||||
scriptpubkey_type: string;
|
||||
scriptpubkey_address: string;
|
||||
value: number;
|
||||
// Elements
|
||||
valuecommitment?: number;
|
||||
asset?: string;
|
||||
pegout?: Pegout;
|
||||
}
|
||||
|
||||
interface Pegout {
|
||||
genesis_hash: string;
|
||||
scriptpubkey: string;
|
||||
scriptpubkey_asm: string;
|
||||
scriptpubkey_address: string;
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
confirmed: boolean;
|
||||
block_height?: number;
|
||||
block_hash?: string;
|
||||
block_time?: number;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
height: number;
|
||||
version: number;
|
||||
timestamp: number;
|
||||
bits: number;
|
||||
nonce: number;
|
||||
difficulty: number;
|
||||
merkle_root: string;
|
||||
tx_count: number;
|
||||
size: number;
|
||||
weight: number;
|
||||
previousblockhash: string;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
address: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
electrum?: boolean;
|
||||
}
|
||||
|
||||
export interface ChainStats {
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
}
|
||||
|
||||
export interface MempoolStats {
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
}
|
||||
|
||||
export interface Outspend {
|
||||
spent: boolean;
|
||||
txid: string;
|
||||
vin: number;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
asset_id: string;
|
||||
issuance_txin: IssuanceTxin;
|
||||
issuance_prevout: IssuancePrevout;
|
||||
reissuance_token: string;
|
||||
contract_hash: string;
|
||||
status: Status;
|
||||
chain_stats: AssetStats;
|
||||
mempool_stats: AssetStats;
|
||||
}
|
||||
|
||||
export interface AssetExtended extends Asset {
|
||||
name: string;
|
||||
ticker: string;
|
||||
precision: number;
|
||||
entity: Entity;
|
||||
version: number;
|
||||
issuer_pubkey: string;
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
interface IssuanceTxin {
|
||||
txid: string;
|
||||
vin: number;
|
||||
}
|
||||
|
||||
interface IssuancePrevout {
|
||||
txid: string;
|
||||
vout: number;
|
||||
}
|
||||
|
||||
interface AssetStats {
|
||||
tx_count: number;
|
||||
issuance_count: number;
|
||||
issued_amount: number;
|
||||
burned_amount: number;
|
||||
has_blinded_issuances: boolean;
|
||||
reissuance_tokens: number;
|
||||
burned_reissuance_tokens: number;
|
||||
peg_in_count: number;
|
||||
peg_in_amount: number;
|
||||
peg_out_count: number;
|
||||
peg_out_amount: number;
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import config from '../../config';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
|
||||
class ElectrsApi implements AbstractBitcoinApi {
|
||||
axiosConfig: AxiosRequestConfig = {
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
constructor() { }
|
||||
|
||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||
return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
||||
return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getBlockHeightTip(): Promise<number> {
|
||||
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getBlockHash(height: number): Promise<string> {
|
||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
||||
throw new Error('Method getAddress not implemented.');
|
||||
}
|
||||
|
||||
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getAddressTransactions not implemented.');
|
||||
}
|
||||
|
||||
$getRawTransactionBitcoind(txId: string): Promise<IEsploraApi.Transaction> {
|
||||
return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
$getAddressPrefix(prefix: string): string[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export default ElectrsApi;
|
||||
@@ -1,122 +1,214 @@
|
||||
import config from '../config';
|
||||
const config = require('../../mempool-config.json');
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import logger from '../logger';
|
||||
import { DB } from '../database';
|
||||
import { IBlock, ITransaction } from '../interfaces';
|
||||
import memPool from './mempool';
|
||||
import { BlockExtended, TransactionExtended } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
class Blocks {
|
||||
private static INITIAL_BLOCK_AMOUNT = 8;
|
||||
private blocks: BlockExtended[] = [];
|
||||
private blocks: IBlock[] = [];
|
||||
private newBlockCallback: Function | undefined;
|
||||
private currentBlockHeight = 0;
|
||||
private lastDifficultyAdjustmentTime = 0;
|
||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||
|
||||
constructor() { }
|
||||
constructor() {
|
||||
setInterval(this.$clearOldTransactionsAndBlocksFromDatabase.bind(this), 86400000);
|
||||
}
|
||||
|
||||
public getBlocks(): BlockExtended[] {
|
||||
public setNewBlockCallback(fn: Function) {
|
||||
this.newBlockCallback = fn;
|
||||
}
|
||||
|
||||
public getBlocks(): IBlock[] {
|
||||
return this.blocks;
|
||||
}
|
||||
|
||||
public setBlocks(blocks: BlockExtended[]) {
|
||||
this.blocks = blocks;
|
||||
public formatBlock(block: IBlock) {
|
||||
return {
|
||||
hash: block.hash,
|
||||
height: block.height,
|
||||
nTx: block.nTx - 1,
|
||||
size: block.size,
|
||||
time: block.time,
|
||||
weight: block.weight,
|
||||
fees: block.fees,
|
||||
minFee: block.minFee,
|
||||
maxFee: block.maxFee,
|
||||
medianFee: block.medianFee,
|
||||
};
|
||||
}
|
||||
|
||||
public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
|
||||
this.newBlockCallbacks.push(fn);
|
||||
}
|
||||
public async updateBlocks() {
|
||||
try {
|
||||
const blockCount = await bitcoinApi.getBlockCount();
|
||||
|
||||
public async $updateBlocks() {
|
||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||
|
||||
if (this.blocks.length === 0) {
|
||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
||||
} else {
|
||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||
}
|
||||
|
||||
if (blockHeightTip - this.currentBlockHeight > Blocks.INITIAL_BLOCK_AMOUNT * 2) {
|
||||
logger.info(`${blockHeightTip - this.currentBlockHeight} blocks since tip. Fast forwarding to the ${Blocks.INITIAL_BLOCK_AMOUNT} recent blocks`);
|
||||
this.currentBlockHeight = blockHeightTip - Blocks.INITIAL_BLOCK_AMOUNT;
|
||||
}
|
||||
|
||||
if (!this.lastDifficultyAdjustmentTime) {
|
||||
const heightDiff = blockHeightTip % 2016;
|
||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||
const block = await bitcoinApi.$getBlock(blockHash);
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
}
|
||||
|
||||
while (this.currentBlockHeight < blockHeightTip) {
|
||||
if (this.currentBlockHeight === 0) {
|
||||
this.currentBlockHeight = blockHeightTip;
|
||||
if (this.blocks.length === 0) {
|
||||
this.currentBlockHeight = blockCount - config.INITIAL_BLOCK_AMOUNT;
|
||||
} else {
|
||||
this.currentBlockHeight++;
|
||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||
this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
|
||||
}
|
||||
|
||||
const transactions: TransactionExtended[] = [];
|
||||
while (this.currentBlockHeight < blockCount) {
|
||||
this.currentBlockHeight++;
|
||||
|
||||
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
||||
const block = await bitcoinApi.$getBlock(blockHash);
|
||||
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||
let block: IBlock | undefined;
|
||||
|
||||
const mempool = memPool.getMempool();
|
||||
let transactionsFound = 0;
|
||||
const storedBlock = await this.$getBlockFromDatabase(this.currentBlockHeight);
|
||||
if (storedBlock) {
|
||||
block = storedBlock;
|
||||
} else {
|
||||
const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
|
||||
block = await bitcoinApi.getBlockAndTransactions(blockHash);
|
||||
|
||||
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.message || e);
|
||||
if (i === 0) {
|
||||
throw new Error('Failed to fetch Coinbase transaction: ' + txIds[i]);
|
||||
const coinbase = await memPool.getRawTransaction(block.tx[0], true);
|
||||
if (coinbase && coinbase.totalOut) {
|
||||
block.fees = coinbase.totalOut;
|
||||
}
|
||||
|
||||
const mempool = memPool.getMempool();
|
||||
let found = 0;
|
||||
let notFound = 0;
|
||||
|
||||
let transactions: ITransaction[] = [];
|
||||
|
||||
for (let i = 1; i < block.tx.length; i++) {
|
||||
if (mempool[block.tx[i]]) {
|
||||
transactions.push(mempool[block.tx[i]]);
|
||||
found++;
|
||||
} else {
|
||||
console.log(`Fetching block tx ${i} of ${block.tx.length}`);
|
||||
const tx = await memPool.getRawTransaction(block.tx[i]);
|
||||
if (tx) {
|
||||
transactions.push(tx);
|
||||
}
|
||||
notFound++;
|
||||
}
|
||||
}
|
||||
|
||||
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
||||
transactions = transactions.filter((tx: ITransaction) => tx.feePerVsize);
|
||||
|
||||
block.minFee = transactions[transactions.length - 1] ? transactions[transactions.length - 1].feePerVsize : 0;
|
||||
block.maxFee = transactions[0] ? transactions[0].feePerVsize : 0;
|
||||
block.medianFee = this.median(transactions.map((tx) => tx.feePerVsize));
|
||||
|
||||
console.log(`New block found (#${this.currentBlockHeight})! `
|
||||
+ `${found} of ${block.tx.length} found in mempool. ${notFound} not found.`);
|
||||
|
||||
if (this.newBlockCallback) {
|
||||
this.newBlockCallback(block);
|
||||
}
|
||||
|
||||
this.$saveBlockToDatabase(block);
|
||||
this.$saveTransactionsToDatabase(block.height, transactions);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${txIds.length - transactionsFound} not found.`);
|
||||
this.blocks.push(block);
|
||||
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
|
||||
this.blocks.shift();
|
||||
}
|
||||
|
||||
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.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
||||
blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
|
||||
blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
|
||||
|
||||
if (block.height % 2016 === 0) {
|
||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||
}
|
||||
|
||||
this.blocks.push(blockExtended);
|
||||
if (this.blocks.length > Blocks.INITIAL_BLOCK_AMOUNT * 4) {
|
||||
this.blocks = this.blocks.slice(-Blocks.INITIAL_BLOCK_AMOUNT * 4);
|
||||
}
|
||||
|
||||
if (this.newBlockCallbacks.length) {
|
||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||
}
|
||||
if (memPool.isInSync()) {
|
||||
diskCache.$saveCacheToDisk();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error getBlockCount', err);
|
||||
}
|
||||
}
|
||||
|
||||
public getLastDifficultyAdjustmentTime(): number {
|
||||
return this.lastDifficultyAdjustmentTime;
|
||||
private async $getBlockFromDatabase(height: number): Promise<IBlock | undefined> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `
|
||||
SELECT * FROM blocks WHERE height = ?
|
||||
`;
|
||||
|
||||
const [rows] = await connection.query<any>(query, [height]);
|
||||
connection.release();
|
||||
|
||||
if (rows[0]) {
|
||||
return rows[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('$get() block error', e);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentBlockHeight(): number {
|
||||
return this.currentBlockHeight;
|
||||
private async $saveBlockToDatabase(block: IBlock) {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `
|
||||
INSERT IGNORE INTO blocks
|
||||
(height, hash, size, weight, minFee, maxFee, time, fees, nTx, medianFee)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params: (any)[] = [
|
||||
block.height,
|
||||
block.hash,
|
||||
block.size,
|
||||
block.weight,
|
||||
block.minFee,
|
||||
block.maxFee,
|
||||
block.time,
|
||||
block.fees,
|
||||
block.nTx - 1,
|
||||
block.medianFee,
|
||||
];
|
||||
|
||||
await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
console.log('$create() block error', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $saveTransactionsToDatabase(blockheight: number, transactions: ITransaction[]) {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
const query = `
|
||||
INSERT IGNORE INTO transactions
|
||||
(blockheight, txid, fee, feePerVsize)
|
||||
VALUES(?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const params: (any)[] = [
|
||||
blockheight,
|
||||
transactions[i].txid,
|
||||
transactions[i].fee,
|
||||
transactions[i].feePerVsize,
|
||||
];
|
||||
|
||||
await connection.query(query, params);
|
||||
}
|
||||
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
console.log('$create() transaction error', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $clearOldTransactionsAndBlocksFromDatabase() {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
let query = `DELETE FROM blocks WHERE height < ?`;
|
||||
await connection.query<any>(query, [this.currentBlockHeight - config.KEEP_BLOCK_AMOUNT]);
|
||||
query = `DELETE FROM transactions WHERE blockheight < ?`;
|
||||
await connection.query<any>(query, [this.currentBlockHeight - config.KEEP_BLOCK_AMOUNT]);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
console.log('$clearOldTransactionsFromDatabase() error', e);
|
||||
}
|
||||
}
|
||||
|
||||
private median(numbers: number[]) {
|
||||
if (!numbers.length) { return 0; }
|
||||
let medianNr = 0;
|
||||
const numsLen = numbers.length;
|
||||
if (numsLen % 2 === 0) {
|
||||
medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
|
||||
} else {
|
||||
medianNr = numbers[(numsLen - 1) / 2];
|
||||
}
|
||||
return medianNr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||
|
||||
export class Common {
|
||||
static median(numbers: number[]) {
|
||||
let medianNr = 0;
|
||||
const numsLen = numbers.length;
|
||||
if (numsLen % 2 === 0) {
|
||||
medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
|
||||
} else {
|
||||
medianNr = numbers[(numsLen - 1) / 2];
|
||||
}
|
||||
return medianNr;
|
||||
}
|
||||
|
||||
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
|
||||
const arr = [transactions[transactions.length - 1].feePerVsize];
|
||||
const chunk = 1 / (rangeLength - 1);
|
||||
let itemsToAdd = rangeLength - 2;
|
||||
|
||||
while (itemsToAdd > 0) {
|
||||
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].feePerVsize);
|
||||
itemsToAdd--;
|
||||
}
|
||||
|
||||
arr.push(transactions[0].feePerVsize);
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
||||
const matches: { [txid: string]: TransactionExtended } = {};
|
||||
deleted
|
||||
// The replaced tx must have at least one input with nSequence < maxint-1 (That’s the opt-in)
|
||||
.filter((tx) => tx.vin.some((vin) => vin.sequence < 0xfffffffe))
|
||||
.forEach((deletedTx) => {
|
||||
const foundMatches = added.find((addedTx) => {
|
||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||
return addedTx.fee > deletedTx.fee
|
||||
// The new transaction must pay more fee per kB than the replaced tx.
|
||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
||||
// Spends one or more of the same inputs
|
||||
&& deletedTx.vin.some((deletedVin) =>
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid));
|
||||
});
|
||||
if (foundMatches) {
|
||||
matches[deletedTx.txid] = foundMatches;
|
||||
}
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
|
||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||
return {
|
||||
txid: tx.txid,
|
||||
fee: tx.fee,
|
||||
vsize: tx.weight / 4,
|
||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
static sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,15 @@
|
||||
import * as fs from 'fs';
|
||||
const fsPromises = fs.promises;
|
||||
import * as cluster from 'cluster';
|
||||
import memPool from './mempool';
|
||||
import blocks from './blocks';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
|
||||
class DiskCache {
|
||||
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + 'cache.json';
|
||||
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + 'cache{number}.json';
|
||||
private static CHUNK_SIZE = 10000;
|
||||
static FILE_NAME = './cache.json';
|
||||
constructor() { }
|
||||
|
||||
async $saveCacheToDisk(): Promise<void> {
|
||||
if (!cluster.isMaster) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
logger.debug('Writing mempool and blocks data to disk cache (async)...');
|
||||
const mempoolChunk_1 = Object.fromEntries(Object.entries(memPool.getMempool()).slice(0, DiskCache.CHUNK_SIZE));
|
||||
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
|
||||
blocks: blocks.getBlocks(),
|
||||
mempool: mempoolChunk_1
|
||||
}), {flag: 'w'});
|
||||
for (let i = 1; i < 10; i++) {
|
||||
const mempoolChunk = Object.fromEntries(
|
||||
Object.entries(memPool.getMempool()).slice(
|
||||
DiskCache.CHUNK_SIZE * i, i === 9 ? undefined : DiskCache.CHUNK_SIZE * i + DiskCache.CHUNK_SIZE
|
||||
)
|
||||
);
|
||||
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
||||
mempool: mempoolChunk
|
||||
}), {flag: 'w'});
|
||||
}
|
||||
logger.debug('Mempool and blocks data saved to disk cache');
|
||||
} catch (e) {
|
||||
logger.warn('Error writing to cache file: ' + e.message || e);
|
||||
}
|
||||
saveData(dataBlob: string) {
|
||||
fs.writeFileSync(DiskCache.FILE_NAME, dataBlob, 'utf8');
|
||||
}
|
||||
|
||||
loadMempoolCache() {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let data: any = {};
|
||||
const cacheData = fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
if (cacheData) {
|
||||
logger.info('Restoring mempool and blocks data from disk cache');
|
||||
data = JSON.parse(cacheData);
|
||||
}
|
||||
|
||||
for (let i = 1; i < 10; i++) {
|
||||
const fileName = DiskCache.FILE_NAMES.replace('{number}', i.toString());
|
||||
if (fs.existsSync(fileName)) {
|
||||
const cacheData2 = JSON.parse(fs.readFileSync(fileName, 'utf8'));
|
||||
Object.assign(data.mempool, cacheData2.mempool);
|
||||
}
|
||||
}
|
||||
|
||||
memPool.setMempool(data.mempool);
|
||||
blocks.setBlocks(data.blocks);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse mempoool and blocks cache. Skipping...');
|
||||
}
|
||||
loadData(): string {
|
||||
return fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import config from '../config';
|
||||
import axios from 'axios';
|
||||
import { DB } from '../database';
|
||||
import logger from '../logger';
|
||||
|
||||
class Donations {
|
||||
private notifyDonationStatusCallback: ((invoiceId: string) => void) | undefined;
|
||||
private options = {
|
||||
baseURL: config.SPONSORS.BTCPAY_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': config.SPONSORS.BTCPAY_AUTH,
|
||||
},
|
||||
timeout: 10000,
|
||||
};
|
||||
|
||||
sponsorsCache: any[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async $updateCache() {
|
||||
try {
|
||||
this.sponsorsCache = await this.$getDonationsFromDatabase('handle, image');
|
||||
} catch (e) {
|
||||
logger.warn('Setting sponsorsCache failed ' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
setNotfyDonationStatusCallback(fn: any): void {
|
||||
this.notifyDonationStatusCallback = fn;
|
||||
}
|
||||
|
||||
async $createRequest(amount: number, orderId: string): Promise<any> {
|
||||
logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
|
||||
|
||||
const postData = {
|
||||
'price': amount,
|
||||
'orderId': orderId,
|
||||
'currency': 'BTC',
|
||||
'itemDesc': 'Sponsor mempool.space',
|
||||
'notificationUrl': config.SPONSORS.BTCPAY_WEBHOOK_URL,
|
||||
'redirectURL': 'https://mempool.space/about',
|
||||
};
|
||||
const response = await axios.post('/invoices', postData, this.options);
|
||||
return {
|
||||
id: response.data.data.id,
|
||||
amount: parseFloat(response.data.data.btcPrice),
|
||||
addresses: response.data.data.addresses,
|
||||
};
|
||||
}
|
||||
|
||||
async $handleWebhookRequest(data: any): Promise<void> {
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
const response = await this.$getStatus(data.id);
|
||||
logger.notice(`Received BTCPayServer webhook. Invoice ID: ${data.id} Status: ${response.status} BTC Paid: ${response.btcPaid}`);
|
||||
if (response.status !== 'complete' && response.status !== 'confirmed' && response.status !== 'paid') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.notifyDonationStatusCallback) {
|
||||
this.notifyDonationStatusCallback(data.id);
|
||||
}
|
||||
|
||||
if (parseFloat(response.btcPaid) < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.orderId !== '') {
|
||||
try {
|
||||
const userData = await this.$getTwitterUserData(response.orderId);
|
||||
const imageUrl = userData.profile_image_url.replace('normal', '200x200');
|
||||
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
|
||||
|
||||
logger.debug('Creating database entry for donation with invoice id: ' + response.id);
|
||||
await this.$addDonationToDatabase(response.btcPaid, userData.screen_name, userData.id, response.id, imageUrl, imageBlob);
|
||||
this.$updateCache();
|
||||
} catch (e) {
|
||||
logger.err(`Error fetching twitter data for handle ${response.orderId}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSponsorImage(id: string): any | undefined {
|
||||
const sponsor = this.sponsorsCache.find((s) => s.handle === id);
|
||||
if (sponsor) {
|
||||
return sponsor.image;
|
||||
}
|
||||
}
|
||||
|
||||
async $getDonationsFromDatabase(fields: string): Promise<any[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `SELECT ${fields} FROM donations ORDER BY id DESC`;
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$getDonationsFromDatabase() error: ' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async $getOldDonations(): Promise<any[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `SELECT * FROM donations WHERE twitter_id IS NULL AND handle != ''`;
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$getLegacyDonations() error' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async $getStatus(id: string): Promise<any> {
|
||||
logger.debug('Fetching status for invoice: ' + id);
|
||||
const response = await axios.get('/invoices/' + id, this.options);
|
||||
logger.debug('Invoice status received: ' + JSON.stringify(response.data));
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
private async $addDonationToDatabase(btcPaid: number, handle: string, twitter_id: number | null,
|
||||
orderId: string, imageUrl: string, image: string): Promise<void> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `INSERT IGNORE INTO donations(added, amount, handle, twitter_id, order_id, imageUrl, image) VALUES (NOW(), ?, ?, ?, ?, ?, FROM_BASE64(?))`;
|
||||
const params: (string | number | null)[] = [
|
||||
btcPaid,
|
||||
handle,
|
||||
twitter_id,
|
||||
orderId,
|
||||
imageUrl,
|
||||
image,
|
||||
];
|
||||
const [result]: any = await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('$addDonationToDatabase() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $updateDonation(id: number, handle: string, twitterId: number, imageUrl: string, image: string): Promise<void> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `UPDATE donations SET handle = ?, twitter_id = ?, imageUrl = ?, image = FROM_BASE64(?) WHERE id = ?`;
|
||||
const params: (string | number)[] = [
|
||||
handle,
|
||||
twitterId,
|
||||
imageUrl,
|
||||
image,
|
||||
id,
|
||||
];
|
||||
const [result]: any = await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('$updateDonation() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private async $getTwitterUserData(handle: string): Promise<any> {
|
||||
logger.debug('Fetching Twitter API data...');
|
||||
const res = await axios.get(`https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + config.SPONSORS.TWITTER_BEARER_AUTH
|
||||
},
|
||||
timeout: 10000,
|
||||
});
|
||||
logger.debug('Twitter user data fetched:' + JSON.stringify(res.data));
|
||||
return res.data;
|
||||
}
|
||||
|
||||
private async $downloadProfileImageBlob(url: string): Promise<string> {
|
||||
logger.debug('Fetching image blob...');
|
||||
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000 });
|
||||
logger.debug('Image downloaded.');
|
||||
return Buffer.from(res.data, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
private async refreshSponsors(): Promise<void> {
|
||||
const oldDonations = await this.$getOldDonations();
|
||||
oldDonations.forEach(async (donation: any) => {
|
||||
logger.debug('Migrating donation for handle: ' + donation.handle);
|
||||
try {
|
||||
const twitterData = await this.$getTwitterUserData(donation.handle);
|
||||
const imageUrl = twitterData.profile_image_url.replace('normal', '200x200');
|
||||
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
|
||||
await this.$updateDonation(donation.id, twitterData.screen_name, twitterData.id, imageUrl, imageBlob);
|
||||
} catch (e) {
|
||||
logger.err('Failed to migrate donation for handle: ' + donation.handle + '. ' + (e.message || e));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Donations();
|
||||
@@ -1,26 +1,26 @@
|
||||
import config from '../config';
|
||||
import { MempoolBlock } from '../mempool.interfaces';
|
||||
import projectedBlocks from './mempool-blocks';
|
||||
import projectedBlocks from './projected-blocks';
|
||||
import { DB } from '../database';
|
||||
|
||||
class FeeApi {
|
||||
constructor() { }
|
||||
|
||||
defaultFee = config.MEMPOOL.NETWORK === 'liquid' ? 0.1 : 1;
|
||||
|
||||
public getRecommendedFee() {
|
||||
const pBlocks = projectedBlocks.getMempoolBlocks();
|
||||
|
||||
const pBlocks = projectedBlocks.getProjectedBlocks();
|
||||
if (!pBlocks.length) {
|
||||
return {
|
||||
'fastestFee': this.defaultFee,
|
||||
'halfHourFee': this.defaultFee,
|
||||
'hourFee': this.defaultFee,
|
||||
'fastestFee': 0,
|
||||
'halfHourFee': 0,
|
||||
'hourFee': 0,
|
||||
};
|
||||
}
|
||||
let firstMedianFee = Math.ceil(pBlocks[0].medianFee);
|
||||
|
||||
const firstMedianFee = this.optimizeMedianFee(pBlocks[0], pBlocks[1]);
|
||||
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
|
||||
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
|
||||
if (pBlocks.length === 1 && pBlocks[0].blockWeight <= 2000000) {
|
||||
firstMedianFee = 1;
|
||||
}
|
||||
|
||||
const secondMedianFee = pBlocks[1] ? Math.ceil(pBlocks[1].medianFee) : firstMedianFee;
|
||||
const thirdMedianFee = pBlocks[2] ? Math.ceil(pBlocks[2].medianFee) : secondMedianFee;
|
||||
|
||||
return {
|
||||
'fastestFee': firstMedianFee,
|
||||
@@ -29,17 +29,19 @@ class FeeApi {
|
||||
};
|
||||
}
|
||||
|
||||
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
|
||||
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
|
||||
if (pBlock.blockVSize <= 500000) {
|
||||
return this.defaultFee;
|
||||
public async $getTransactionsForBlock(blockHeight: number): Promise<any[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `SELECT feePerVsize AS fpv FROM transactions WHERE blockheight = ? ORDER BY feePerVsize ASC`;
|
||||
const [rows] = await connection.query<any>(query, [blockHeight]);
|
||||
connection.release();
|
||||
return rows;
|
||||
} catch (e) {
|
||||
console.log('$getTransactionsForBlock() error', e);
|
||||
return [];
|
||||
}
|
||||
if (pBlock.blockVSize <= 950000 && nextBlock) {
|
||||
const multiplier = (pBlock.blockVSize - 500000) / 500000;
|
||||
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
|
||||
}
|
||||
return Math.round(useFee);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new FeeApi();
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
import logger from '../logger';
|
||||
import axios from 'axios';
|
||||
import { IConversionRates } from '../mempool.interfaces';
|
||||
import * as request from 'request';
|
||||
|
||||
class FiatConversion {
|
||||
private conversionRates: IConversionRates = {
|
||||
'USD': 0
|
||||
private tickers = {
|
||||
'BTCUSD': {
|
||||
'USD': 4110.78
|
||||
},
|
||||
};
|
||||
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
|
||||
this.ratesChangedCallback = fn;
|
||||
}
|
||||
|
||||
public startService() {
|
||||
logger.info('Starting currency rates service');
|
||||
setInterval(this.updateCurrency.bind(this), 1000 * 60 * 60);
|
||||
this.updateCurrency();
|
||||
}
|
||||
|
||||
public getConversionRates() {
|
||||
return this.conversionRates;
|
||||
public getTickers() {
|
||||
return this.tickers;
|
||||
}
|
||||
|
||||
private async updateCurrency(): Promise<void> {
|
||||
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,
|
||||
};
|
||||
if (this.ratesChangedCallback) {
|
||||
this.ratesChangedCallback(this.conversionRates);
|
||||
private updateCurrency() {
|
||||
request('https://api.opennode.co/v1/rates', { json: true }, (err, res, body) => {
|
||||
if (err) { return console.log(err); }
|
||||
if (body && body.data) {
|
||||
this.tickers = body.data;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('Error updating fiat conversion rates: ' + e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ILoadingIndicators } from '../mempool.interfaces';
|
||||
|
||||
class LoadingIndicators {
|
||||
private loadingIndicators: ILoadingIndicators = {
|
||||
'mempool': 0,
|
||||
};
|
||||
private progressChangedCallback: ((loadingIndicators: ILoadingIndicators) => void) | undefined;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public setProgressChangedCallback(fn: (loadingIndicators: ILoadingIndicators) => void) {
|
||||
this.progressChangedCallback = fn;
|
||||
}
|
||||
|
||||
public setProgress(name: string, progressPercent: number) {
|
||||
const newProgress = Math.round(progressPercent);
|
||||
if (newProgress >= 100) {
|
||||
delete this.loadingIndicators[name];
|
||||
} else {
|
||||
this.loadingIndicators[name] = newProgress;
|
||||
}
|
||||
if (this.progressChangedCallback) {
|
||||
this.progressChangedCallback(this.loadingIndicators);
|
||||
}
|
||||
}
|
||||
|
||||
public getLoadingIndicators() {
|
||||
return this.loadingIndicators;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LoadingIndicators();
|
||||
@@ -1,38 +0,0 @@
|
||||
interface ICache {
|
||||
type: string;
|
||||
id: string;
|
||||
expires: Date;
|
||||
data: any;
|
||||
}
|
||||
|
||||
class MemoryCache {
|
||||
private cache: ICache[] = [];
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000);
|
||||
}
|
||||
|
||||
public set(type: string, id: string, data: any, secondsExpiry: number) {
|
||||
const expiry = new Date();
|
||||
expiry.setSeconds(expiry.getSeconds() + secondsExpiry);
|
||||
this.cache.push({
|
||||
type: type,
|
||||
id: id,
|
||||
data: data,
|
||||
expires: expiry,
|
||||
});
|
||||
}
|
||||
|
||||
public get<T>(type: string, id: string): T | null {
|
||||
const found = this.cache.find((cache) => cache.type === type && cache.id === id);
|
||||
if (found) {
|
||||
return found.data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
this.cache = this.cache.filter((cache) => cache.expires < (new Date()));
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemoryCache();
|
||||
@@ -1,86 +0,0 @@
|
||||
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
|
||||
class MempoolBlocks {
|
||||
private static DEFAULT_PROJECTED_BLOCKS_AMOUNT = 8;
|
||||
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public getMempoolBlocks(): MempoolBlock[] {
|
||||
return this.mempoolBlocks.map((block) => {
|
||||
return {
|
||||
blockSize: block.blockSize,
|
||||
blockVSize: block.blockVSize,
|
||||
nTx: block.nTx,
|
||||
totalFees: block.totalFees,
|
||||
medianFee: block.medianFee,
|
||||
feeRange: block.feeRange,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public getMempoolBlocksWithTransactions(): MempoolBlockWithTransactions[] {
|
||||
return this.mempoolBlocks;
|
||||
}
|
||||
|
||||
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
|
||||
const latestMempool = memPool;
|
||||
const memPoolArray: TransactionExtended[] = [];
|
||||
for (const i in latestMempool) {
|
||||
if (latestMempool.hasOwnProperty(i)) {
|
||||
memPoolArray.push(latestMempool[i]);
|
||||
}
|
||||
}
|
||||
memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
||||
const transactionsSorted = memPoolArray.filter((tx) => tx.feePerVsize);
|
||||
this.mempoolBlocks = this.calculateMempoolBlocks(transactionsSorted);
|
||||
}
|
||||
|
||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
|
||||
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||
let blockVSize = 0;
|
||||
let blockSize = 0;
|
||||
let transactions: TransactionExtended[] = [];
|
||||
transactionsSorted.forEach((tx) => {
|
||||
if (blockVSize + tx.vsize <= 1000000 || mempoolBlocks.length === MempoolBlocks.DEFAULT_PROJECTED_BLOCKS_AMOUNT - 1) {
|
||||
blockVSize += tx.vsize;
|
||||
blockSize += tx.size;
|
||||
transactions.push(tx);
|
||||
} else {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
||||
blockVSize = tx.vsize;
|
||||
blockSize = tx.size;
|
||||
transactions = [tx];
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockVSize, mempoolBlocks.length));
|
||||
}
|
||||
return mempoolBlocks;
|
||||
}
|
||||
|
||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||
blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlockWithTransactions {
|
||||
let rangeLength = 4;
|
||||
if (blocksIndex === 0) {
|
||||
rangeLength = 8;
|
||||
}
|
||||
if (transactions.length > 4000) {
|
||||
rangeLength = 6;
|
||||
} else if (transactions.length > 10000) {
|
||||
rangeLength = 8;
|
||||
}
|
||||
return {
|
||||
blockSize: blockSize,
|
||||
blockVSize: blockVSize,
|
||||
nTx: transactions.length,
|
||||
totalFees: transactions.reduce((acc, cur) => acc + cur.fee, 0),
|
||||
medianFee: Common.median(transactions.map((tx) => tx.feePerVsize)),
|
||||
feeRange: Common.getFeesInRange(transactions, rangeLength),
|
||||
transactionIds: transactions.map((tx) => tx.txid),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MempoolBlocks();
|
||||
@@ -1,69 +1,35 @@
|
||||
import config from '../config';
|
||||
const config = require('../../mempool-config.json');
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
|
||||
import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
import transactionUtils from './transaction-utils';
|
||||
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
||||
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
||||
import loadingIndicators from './loading-indicators';
|
||||
import { ITransaction, IMempoolInfo, IMempool } from '../interfaces';
|
||||
|
||||
class Mempool {
|
||||
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
|
||||
private static CLEAR_PROTECTION_MINUTES = 10;
|
||||
private inSync: boolean = false;
|
||||
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
|
||||
maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 };
|
||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||
private mempool: IMempool = {};
|
||||
private mempoolInfo: IMempoolInfo | undefined;
|
||||
private mempoolChangedCallback: Function | undefined;
|
||||
|
||||
private txPerSecondArray: number[] = [];
|
||||
private txPerSecond: number = 0;
|
||||
|
||||
private vBytesPerSecondArray: VbytesPerSecond[] = [];
|
||||
private vBytesPerSecondArray: any[] = [];
|
||||
private vBytesPerSecond: number = 0;
|
||||
private mempoolProtection = 0;
|
||||
private latestTransactions: any[] = [];
|
||||
|
||||
constructor() {
|
||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||
}
|
||||
|
||||
public isInSync(): boolean {
|
||||
return this.inSync;
|
||||
}
|
||||
|
||||
public setOutOfSync(): void {
|
||||
this.inSync = false;
|
||||
loadingIndicators.setProgress('mempool', 99);
|
||||
}
|
||||
|
||||
public getLatestTransactions() {
|
||||
return this.latestTransactions;
|
||||
}
|
||||
|
||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: TransactionExtended; },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) => void) {
|
||||
public setMempoolChangedCallback(fn: Function) {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
public getMempool(): { [txid: string]: TransactionExtended } {
|
||||
return this.mempoolCache;
|
||||
public getMempool(): { [txid: string]: ITransaction } {
|
||||
return this.mempool;
|
||||
}
|
||||
|
||||
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
|
||||
this.mempoolCache = mempoolData;
|
||||
if (this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
||||
}
|
||||
public setMempool(mempoolData: any) {
|
||||
this.mempool = mempoolData;
|
||||
}
|
||||
|
||||
public async $updateMemPoolInfo() {
|
||||
this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
|
||||
}
|
||||
|
||||
public getMempoolInfo(): IBitcoinApi.MempoolInfo | undefined {
|
||||
public getMempoolInfo(): IMempoolInfo | undefined {
|
||||
return this.mempoolInfo;
|
||||
}
|
||||
|
||||
@@ -75,128 +41,125 @@ class Mempool {
|
||||
return this.vBytesPerSecond;
|
||||
}
|
||||
|
||||
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
||||
const txTimes: number[] = [];
|
||||
txIds.forEach((txId: string) => {
|
||||
const tx = this.mempoolCache[txId];
|
||||
if (tx && tx.firstSeen) {
|
||||
txTimes.push(tx.firstSeen);
|
||||
} else {
|
||||
txTimes.push(0);
|
||||
}
|
||||
});
|
||||
return txTimes;
|
||||
public async updateMemPoolInfo() {
|
||||
try {
|
||||
this.mempoolInfo = await bitcoinApi.getMempoolInfo();
|
||||
} catch (err) {
|
||||
console.log('Error getMempoolInfo', err);
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateMempool() {
|
||||
logger.debug('Updating mempool');
|
||||
public async getRawTransaction(txId: string, isCoinbase = false): Promise<ITransaction | false> {
|
||||
try {
|
||||
const transaction = await bitcoinApi.getRawTransaction(txId);
|
||||
|
||||
let totalOut = 0;
|
||||
transaction.vout.forEach((output) => totalOut += output.value);
|
||||
|
||||
if (config.BACKEND_API === 'electrs') {
|
||||
transaction.feePerWeightUnit = (transaction.fee * 100000000) / transaction.weight || 0;
|
||||
transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
|
||||
transaction.totalOut = totalOut / 100000000;
|
||||
} else {
|
||||
let totalIn = 0;
|
||||
if (!isCoinbase) {
|
||||
for (let i = 0; i < transaction.vin.length; i++) {
|
||||
try {
|
||||
const result = await bitcoinApi.getRawTransaction(transaction.vin[i].txid);
|
||||
transaction.vin[i]['value'] = result.vout[transaction.vin[i].vout].value;
|
||||
totalIn += result.vout[transaction.vin[i].vout].value;
|
||||
} catch (err) {
|
||||
console.log('Locating historical tx error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalIn > totalOut) {
|
||||
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
|
||||
transaction.feePerWeightUnit = (transaction.fee * 100000000) / (transaction.vsize * 4) || 0;
|
||||
transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
|
||||
} else if (!isCoinbase) {
|
||||
transaction.fee = 0;
|
||||
transaction.feePerVsize = 0;
|
||||
transaction.feePerWeightUnit = 0;
|
||||
console.log('Minus fee error!');
|
||||
}
|
||||
transaction.totalOut = totalOut;
|
||||
}
|
||||
return transaction;
|
||||
} catch (e) {
|
||||
console.log(txId + ' not found');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async updateMempool() {
|
||||
console.log('Updating mempool');
|
||||
const start = new Date().getTime();
|
||||
let hasChange: boolean = false;
|
||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||
let txCount = 0;
|
||||
const transactions = await bitcoinApi.$getRawMempool();
|
||||
const diff = transactions.length - currentMempoolSize;
|
||||
const newTransactions: TransactionExtended[] = [];
|
||||
|
||||
if (!this.inSync) {
|
||||
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
|
||||
}
|
||||
|
||||
for (const txid of transactions) {
|
||||
if (!this.mempoolCache[txid]) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
||||
this.mempoolCache[txid] = transaction;
|
||||
txCount++;
|
||||
if (this.inSync) {
|
||||
try {
|
||||
const transactions = await bitcoinApi.getRawMempool();
|
||||
const diff = transactions.length - Object.keys(this.mempool).length;
|
||||
for (const tx of transactions) {
|
||||
if (!this.mempool[tx]) {
|
||||
const transaction = await this.getRawTransaction(tx);
|
||||
if (transaction) {
|
||||
this.mempool[tx] = transaction;
|
||||
txCount++;
|
||||
this.txPerSecondArray.push(new Date().getTime());
|
||||
this.vBytesPerSecondArray.push({
|
||||
unixTime: new Date().getTime(),
|
||||
vSize: transaction.vsize,
|
||||
});
|
||||
}
|
||||
hasChange = true;
|
||||
if (diff > 0) {
|
||||
logger.debug('Fetched transaction ' + txCount + ' / ' + diff);
|
||||
hasChange = true;
|
||||
if (diff > 0) {
|
||||
console.log('Calculated fee for transaction ' + txCount + ' / ' + diff);
|
||||
} else {
|
||||
console.log('Calculated fee for transaction ' + txCount);
|
||||
}
|
||||
} else {
|
||||
logger.debug('Fetched transaction ' + txCount);
|
||||
console.log('Error finding transaction in mempool.');
|
||||
}
|
||||
newTransactions.push(transaction);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
||||
}
|
||||
|
||||
if ((new Date().getTime()) - start > config.MEMPOOL_REFRESH_RATE_MS * 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
||||
if (this.mempoolProtection === 0
|
||||
&& config.MEMPOOL.BACKEND === 'esplora'
|
||||
&& currentMempoolSize > 20000
|
||||
&& transactions.length / currentMempoolSize <= 0.80
|
||||
) {
|
||||
this.mempoolProtection = 1;
|
||||
this.inSync = false;
|
||||
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
|
||||
setTimeout(() => {
|
||||
this.mempoolProtection = 2;
|
||||
logger.warn('Mempool clear protection resumed.');
|
||||
}, 1000 * 60 * Mempool.CLEAR_PROTECTION_MINUTES);
|
||||
}
|
||||
|
||||
let newMempool = {};
|
||||
const deletedTransactions: TransactionExtended[] = [];
|
||||
|
||||
if (this.mempoolProtection !== 1) {
|
||||
this.mempoolProtection = 0;
|
||||
// Index object for faster search
|
||||
const transactionsObject = {};
|
||||
transactions.forEach((txId) => transactionsObject[txId] = true);
|
||||
|
||||
// Replace mempool to separate deleted transactions
|
||||
for (const tx in this.mempoolCache) {
|
||||
if (transactionsObject[tx]) {
|
||||
newMempool[tx] = this.mempoolCache[tx];
|
||||
const newMempool: IMempool = {};
|
||||
transactions.forEach((tx) => {
|
||||
if (this.mempool[tx]) {
|
||||
newMempool[tx] = this.mempool[tx];
|
||||
} else {
|
||||
deletedTransactions.push(this.mempoolCache[tx]);
|
||||
hasChange = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.mempool = newMempool;
|
||||
|
||||
if (hasChange && this.mempoolChangedCallback) {
|
||||
this.mempoolChangedCallback(this.mempool);
|
||||
}
|
||||
} else {
|
||||
newMempool = this.mempoolCache;
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
console.log('Mempool updated in ' + time / 1000 + ' seconds');
|
||||
} catch (err) {
|
||||
console.log('getRawMempool error.', err);
|
||||
}
|
||||
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
|
||||
this.inSync = true;
|
||||
logger.info('The mempool is now in sync!');
|
||||
loadingIndicators.setProgress('mempool', 100);
|
||||
}
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolCache = newMempool;
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||
}
|
||||
|
||||
const end = new Date().getTime();
|
||||
const time = end - start;
|
||||
logger.debug(`New mempool size: ${Object.keys(newMempool).length} Change: ${diff}`);
|
||||
logger.debug('Mempool updated in ' + time / 1000 + ' seconds');
|
||||
}
|
||||
|
||||
private updateTxPerSecond() {
|
||||
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
|
||||
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.TX_PER_SECOND_SPAN_SECONDS);
|
||||
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
|
||||
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;
|
||||
this.txPerSecond = this.txPerSecondArray.length / config.TX_PER_SECOND_SPAN_SECONDS || 0;
|
||||
|
||||
this.vBytesPerSecondArray = this.vBytesPerSecondArray.filter((data) => data.unixTime > nowMinusTimeSpan);
|
||||
if (this.vBytesPerSecondArray.length) {
|
||||
this.vBytesPerSecond = Math.round(
|
||||
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD
|
||||
this.vBytesPerSecondArray.map((data) => data.vSize).reduce((a, b) => a + b) / config.TX_PER_SECOND_SPAN_SECONDS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
101
backend/src/api/projected-blocks.ts
Normal file
101
backend/src/api/projected-blocks.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
const config = require('../../mempool-config.json');
|
||||
import { ITransaction, IProjectedBlock, IMempool, IProjectedBlockInternal } from '../interfaces';
|
||||
|
||||
class ProjectedBlocks {
|
||||
private transactionsSorted: ITransaction[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public getProjectedBlockFeesForBlock(index: number) {
|
||||
const projectedBlock = this.getProjectedBlocksInternal()[index];
|
||||
|
||||
if (!projectedBlock) {
|
||||
throw new Error('No projected block for that index');
|
||||
}
|
||||
|
||||
return projectedBlock.txFeePerVsizes.map((fpv) => {
|
||||
return {'fpv': fpv};
|
||||
});
|
||||
}
|
||||
|
||||
public updateProjectedBlocks(memPool: IMempool): void {
|
||||
const latestMempool = memPool;
|
||||
const memPoolArray: ITransaction[] = [];
|
||||
for (const i in latestMempool) {
|
||||
if (latestMempool.hasOwnProperty(i)) {
|
||||
memPoolArray.push(latestMempool[i]);
|
||||
}
|
||||
}
|
||||
memPoolArray.sort((a, b) => b.feePerWeightUnit - a.feePerWeightUnit);
|
||||
this.transactionsSorted = memPoolArray.filter((tx) => tx.feePerWeightUnit);
|
||||
}
|
||||
|
||||
public getProjectedBlocks(txId?: string, numberOfBlocks: number = config.DEFAULT_PROJECTED_BLOCKS_AMOUNT): IProjectedBlock[] {
|
||||
return this.getProjectedBlocksInternal(numberOfBlocks).map((projectedBlock) => {
|
||||
return {
|
||||
blockSize: projectedBlock.blockSize,
|
||||
blockWeight: projectedBlock.blockWeight,
|
||||
nTx: projectedBlock.nTx,
|
||||
minFee: projectedBlock.minFee,
|
||||
maxFee: projectedBlock.maxFee,
|
||||
minWeightFee: projectedBlock.minWeightFee,
|
||||
maxWeightFee: projectedBlock.maxWeightFee,
|
||||
medianFee: projectedBlock.medianFee,
|
||||
fees: projectedBlock.fees,
|
||||
hasMytx: txId ? projectedBlock.txIds.some((tx) => tx === txId) : false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getProjectedBlocksInternal(numberOfBlocks: number = config.DEFAULT_PROJECTED_BLOCKS_AMOUNT): IProjectedBlockInternal[] {
|
||||
const projectedBlocks: IProjectedBlockInternal[] = [];
|
||||
let blockWeight = 0;
|
||||
let blockSize = 0;
|
||||
let transactions: ITransaction[] = [];
|
||||
this.transactionsSorted.forEach((tx) => {
|
||||
if (blockWeight + tx.vsize * 4 < 4000000 || projectedBlocks.length === numberOfBlocks) {
|
||||
blockWeight += tx.weight || tx.vsize * 4;
|
||||
blockSize += tx.size;
|
||||
transactions.push(tx);
|
||||
} else {
|
||||
projectedBlocks.push(this.dataToProjectedBlock(transactions, blockSize, blockWeight));
|
||||
blockWeight = 0;
|
||||
blockSize = 0;
|
||||
transactions = [];
|
||||
}
|
||||
});
|
||||
if (transactions.length) {
|
||||
projectedBlocks.push(this.dataToProjectedBlock(transactions, blockSize, blockWeight));
|
||||
}
|
||||
return projectedBlocks;
|
||||
}
|
||||
|
||||
private dataToProjectedBlock(transactions: ITransaction[], blockSize: number, blockWeight: number): IProjectedBlockInternal {
|
||||
return {
|
||||
blockSize: blockSize,
|
||||
blockWeight: blockWeight,
|
||||
nTx: transactions.length,
|
||||
minFee: transactions[transactions.length - 1].feePerVsize,
|
||||
maxFee: transactions[0].feePerVsize,
|
||||
minWeightFee: transactions[transactions.length - 1].feePerWeightUnit,
|
||||
maxWeightFee: transactions[0].feePerWeightUnit,
|
||||
medianFee: this.median(transactions.map((tx) => tx.feePerVsize)),
|
||||
txIds: transactions.map((tx) => tx.txid),
|
||||
txFeePerVsizes: transactions.map((tx) => tx.feePerVsize).reverse(),
|
||||
fees: transactions.map((tx) => tx.fee).reduce((acc, currValue) => acc + currValue),
|
||||
};
|
||||
}
|
||||
|
||||
private median(numbers: number[]) {
|
||||
let medianNr = 0;
|
||||
const numsLen = numbers.length;
|
||||
if (numsLen % 2 === 0) {
|
||||
medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
|
||||
} else {
|
||||
medianNr = numbers[(numsLen - 1) / 2];
|
||||
}
|
||||
return medianNr;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ProjectedBlocks();
|
||||
@@ -1,26 +1,20 @@
|
||||
import memPool from './mempool';
|
||||
import { DB } from '../database';
|
||||
import logger from '../logger';
|
||||
|
||||
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
|
||||
import { ITransaction, IMempoolStats } from '../interfaces';
|
||||
|
||||
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': [],
|
||||
};
|
||||
protected newStatisticsEntryCallback: Function | undefined;
|
||||
|
||||
public setNewStatisticsEntryCallback(fn: (stats: OptimizedStatistic) => void) {
|
||||
public setNewStatisticsEntryCallback(fn: Function) {
|
||||
this.newStatisticsEntryCallback = fn;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public startStatistics(): void {
|
||||
logger.info('Starting statistics service');
|
||||
|
||||
const now = new Date();
|
||||
const nextInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(),
|
||||
Math.floor(now.getMinutes() / 1) * 1 + 1, 0, 0);
|
||||
@@ -28,61 +22,57 @@ class Statistics {
|
||||
|
||||
setTimeout(() => {
|
||||
this.runStatistics();
|
||||
this.intervalTimer = setInterval(() => {
|
||||
this.runStatistics();
|
||||
}, 1 * 60 * 1000);
|
||||
this.intervalTimer = setInterval(() => { 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();
|
||||
logger.debug('Statistics cache created');
|
||||
}
|
||||
|
||||
private async runStatistics(): Promise<void> {
|
||||
if (!memPool.isInSync()) {
|
||||
return;
|
||||
}
|
||||
const currentMempool = memPool.getMempool();
|
||||
const txPerSecond = memPool.getTxPerSecond();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
|
||||
logger.debug('Running statistics');
|
||||
if (txPerSecond === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let memPoolArray: TransactionExtended[] = [];
|
||||
console.log('Running statistics');
|
||||
|
||||
let memPoolArray: ITransaction[] = [];
|
||||
for (const i in currentMempool) {
|
||||
if (currentMempool.hasOwnProperty(i)) {
|
||||
memPoolArray.push(currentMempool[i]);
|
||||
}
|
||||
}
|
||||
// Remove 0 and undefined
|
||||
memPoolArray = memPoolArray.filter((tx) => tx.feePerVsize);
|
||||
memPoolArray = memPoolArray.filter((tx) => tx.feePerWeightUnit);
|
||||
|
||||
if (!memPoolArray.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
memPoolArray.sort((a, b) => a.feePerVsize - b.feePerVsize);
|
||||
memPoolArray.sort((a, b) => a.feePerWeightUnit - b.feePerWeightUnit);
|
||||
const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4;
|
||||
const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr);
|
||||
|
||||
const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
const weightUnitFees: { [feePerWU: number]: number } = {};
|
||||
const weightVsizeFees: { [feePerWU: number]: number } = {};
|
||||
|
||||
memPoolArray.forEach((transaction) => {
|
||||
for (let i = 0; i < logFees.length; i++) {
|
||||
if ((logFees[i] === 2000 && transaction.feePerWeightUnit >= 2000) || transaction.feePerWeightUnit <= logFees[i]) {
|
||||
if (weightUnitFees[logFees[i]]) {
|
||||
weightUnitFees[logFees[i]] += transaction.vsize * 4;
|
||||
} else {
|
||||
weightUnitFees[logFees[i]] = transaction.vsize * 4;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
memPoolArray.forEach((transaction) => {
|
||||
for (let i = 0; i < logFees.length; i++) {
|
||||
if ((logFees[i] === 2000 && transaction.feePerVsize >= 2000) || transaction.feePerVsize <= logFees[i]) {
|
||||
@@ -103,7 +93,10 @@ class Statistics {
|
||||
vbytes_per_second: Math.round(vBytesPerSecond),
|
||||
mempool_byte_weight: totalWeight,
|
||||
total_fee: totalFee,
|
||||
fee_data: '',
|
||||
fee_data: JSON.stringify({
|
||||
'wu': weightUnitFees,
|
||||
'vsize': weightVsizeFees
|
||||
}),
|
||||
vsize_1: weightVsizeFees['1'] || 0,
|
||||
vsize_2: weightVsizeFees['2'] || 0,
|
||||
vsize_3: weightVsizeFees['3'] || 0,
|
||||
@@ -146,13 +139,11 @@ class Statistics {
|
||||
|
||||
if (this.newStatisticsEntryCallback && insertId) {
|
||||
const newStats = await this.$get(insertId);
|
||||
if (newStats) {
|
||||
this.newStatisticsEntryCallback(newStats);
|
||||
}
|
||||
this.newStatisticsEntryCallback(newStats);
|
||||
}
|
||||
}
|
||||
|
||||
private async $create(statistics: Statistic): Promise<number | undefined> {
|
||||
private async $create(statistics: IMempoolStats): Promise<number | undefined> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `INSERT INTO statistics(
|
||||
@@ -255,212 +246,144 @@ class Statistics {
|
||||
connection.release();
|
||||
return result.insertId;
|
||||
} catch (e) {
|
||||
logger.err('$create() error' + e.message || e);
|
||||
console.log('$create() error', e);
|
||||
}
|
||||
}
|
||||
|
||||
private getQueryForDays(div: number) {
|
||||
private getQueryForDays(days: number, groupBy: number) {
|
||||
|
||||
return `SELECT id, added, unconfirmed_transactions,
|
||||
tx_per_second,
|
||||
vbytes_per_second,
|
||||
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 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${div} ORDER BY id DESC LIMIT 480`;
|
||||
AVG(tx_per_second) AS tx_per_second,
|
||||
AVG(vbytes_per_second) AS vbytes_per_second,
|
||||
AVG(vsize_1) AS vsize_1,
|
||||
AVG(vsize_2) AS vsize_2,
|
||||
AVG(vsize_3) AS vsize_3,
|
||||
AVG(vsize_4) AS vsize_4,
|
||||
AVG(vsize_5) AS vsize_5,
|
||||
AVG(vsize_6) AS vsize_6,
|
||||
AVG(vsize_8) AS vsize_8,
|
||||
AVG(vsize_10) AS vsize_10,
|
||||
AVG(vsize_12) AS vsize_12,
|
||||
AVG(vsize_15) AS vsize_15,
|
||||
AVG(vsize_20) AS vsize_20,
|
||||
AVG(vsize_30) AS vsize_30,
|
||||
AVG(vsize_40) AS vsize_40,
|
||||
AVG(vsize_50) AS vsize_50,
|
||||
AVG(vsize_60) AS vsize_60,
|
||||
AVG(vsize_70) AS vsize_70,
|
||||
AVG(vsize_80) AS vsize_80,
|
||||
AVG(vsize_90) AS vsize_90,
|
||||
AVG(vsize_100) AS vsize_100,
|
||||
AVG(vsize_125) AS vsize_125,
|
||||
AVG(vsize_150) AS vsize_150,
|
||||
AVG(vsize_175) AS vsize_175,
|
||||
AVG(vsize_200) AS vsize_200,
|
||||
AVG(vsize_250) AS vsize_250,
|
||||
AVG(vsize_300) AS vsize_300,
|
||||
AVG(vsize_350) AS vsize_350,
|
||||
AVG(vsize_400) AS vsize_400,
|
||||
AVG(vsize_500) AS vsize_500,
|
||||
AVG(vsize_600) AS vsize_600,
|
||||
AVG(vsize_700) AS vsize_700,
|
||||
AVG(vsize_800) AS vsize_800,
|
||||
AVG(vsize_900) AS vsize_900,
|
||||
AVG(vsize_1000) AS vsize_1000,
|
||||
AVG(vsize_1200) AS vsize_1200,
|
||||
AVG(vsize_1400) AS vsize_1400,
|
||||
AVG(vsize_1600) AS vsize_1600,
|
||||
AVG(vsize_1800) AS vsize_1800,
|
||||
AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`;
|
||||
}
|
||||
|
||||
public async $get(id: number): Promise<OptimizedStatistic | undefined> {
|
||||
public async $get(id: number): Promise<IMempoolStats | undefined> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `SELECT * FROM statistics WHERE id = ?`;
|
||||
const [rows] = await connection.query<any>(query, [id]);
|
||||
connection.release();
|
||||
if (rows[0]) {
|
||||
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
||||
}
|
||||
return rows[0];
|
||||
} catch (e) {
|
||||
logger.err('$list2H() error' + e.message || e);
|
||||
console.log('$list2H() error', e);
|
||||
}
|
||||
}
|
||||
|
||||
public async $list2H(): Promise<OptimizedStatistic[]> {
|
||||
public async $list2H(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = `SELECT * FROM statistics ORDER BY id DESC LIMIT 120`;
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list2H() error' + e.message || e);
|
||||
console.log('$list2H() error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list24H(): Promise<OptimizedStatistic[]> {
|
||||
public async $list24H(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(180);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const query = this.getQueryForDays(120, 720);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list24h() error' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list1W(): Promise<OptimizedStatistic[]> {
|
||||
public async $list1W(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(1260);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const query = this.getQueryForDays(120, 5040);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list1W() error' + e);
|
||||
console.log('$list1W() error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list1M(): Promise<OptimizedStatistic[]> {
|
||||
public async $list1M(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(5040);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const query = this.getQueryForDays(120, 20160);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list1M() error' + e);
|
||||
console.log('$list1M() error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list3M(): Promise<OptimizedStatistic[]> {
|
||||
public async $list3M(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(15120);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const query = this.getQueryForDays(120, 60480);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list3M() error' + e);
|
||||
console.log('$list3M() error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list6M(): Promise<OptimizedStatistic[]> {
|
||||
public async $list6M(): Promise<IMempoolStats[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(30240);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
const query = this.getQueryForDays(120, 120960);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$list6M() error' + e);
|
||||
console.log('$list6M() error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async $list1Y(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(60480);
|
||||
const [rows] = await connection.query<any>({ sql: query, timeout: this.queryTimeout });
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
} catch (e) {
|
||||
logger.err('$list6M() error' + e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
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,
|
||||
vsizes: [
|
||||
s.vsize_1,
|
||||
s.vsize_2,
|
||||
s.vsize_3,
|
||||
s.vsize_4,
|
||||
s.vsize_5,
|
||||
s.vsize_6,
|
||||
s.vsize_8,
|
||||
s.vsize_10,
|
||||
s.vsize_12,
|
||||
s.vsize_15,
|
||||
s.vsize_20,
|
||||
s.vsize_30,
|
||||
s.vsize_40,
|
||||
s.vsize_50,
|
||||
s.vsize_60,
|
||||
s.vsize_70,
|
||||
s.vsize_80,
|
||||
s.vsize_90,
|
||||
s.vsize_100,
|
||||
s.vsize_125,
|
||||
s.vsize_150,
|
||||
s.vsize_175,
|
||||
s.vsize_200,
|
||||
s.vsize_250,
|
||||
s.vsize_300,
|
||||
s.vsize_350,
|
||||
s.vsize_400,
|
||||
s.vsize_500,
|
||||
s.vsize_600,
|
||||
s.vsize_700,
|
||||
s.vsize_800,
|
||||
s.vsize_900,
|
||||
s.vsize_1000,
|
||||
s.vsize_1200,
|
||||
s.vsize_1400,
|
||||
s.vsize_1600,
|
||||
s.vsize_1800,
|
||||
s.vsize_2000,
|
||||
]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new Statistics();
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import logger from '../logger';
|
||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||
|
||||
class TransactionUtils {
|
||||
constructor() { }
|
||||
|
||||
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
||||
return {
|
||||
vin: [{
|
||||
scriptsig: tx.vin[0].scriptsig || tx.vin[0]['coinbase']
|
||||
}],
|
||||
vout: tx.vout
|
||||
.map((vout) => ({
|
||||
scriptpubkey_address: vout.scriptpubkey_address,
|
||||
value: vout.value
|
||||
}))
|
||||
.filter((vout) => vout.value)
|
||||
};
|
||||
}
|
||||
|
||||
public async $getTransactionExtended(txId: string, addPrevouts = false): Promise<TransactionExtended> {
|
||||
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
|
||||
return this.extendTransaction(transaction);
|
||||
}
|
||||
|
||||
private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
|
||||
const transactionExtended: TransactionExtended = Object.assign({
|
||||
vsize: Math.round(transaction.weight / 4),
|
||||
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
|
||||
}, transaction);
|
||||
if (!transaction.status.confirmed) {
|
||||
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
|
||||
}
|
||||
return transactionExtended;
|
||||
}
|
||||
}
|
||||
|
||||
export default new TransactionUtils();
|
||||
@@ -1,451 +0,0 @@
|
||||
import logger from '../logger';
|
||||
import * as WebSocket from 'ws';
|
||||
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
|
||||
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
import backendInfo from './backend-info';
|
||||
import mempoolBlocks from './mempool-blocks';
|
||||
import fiatConversion from './fiat-conversion';
|
||||
import { Common } from './common';
|
||||
import loadingIndicators from './loading-indicators';
|
||||
import config from '../config';
|
||||
import transactionUtils from './transaction-utils';
|
||||
|
||||
class WebsocketHandler {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d';
|
||||
private extraInitProperties = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
setWebsocketServer(wss: WebSocket.Server) {
|
||||
this.wss = wss;
|
||||
}
|
||||
|
||||
setExtraInitProperties(property: string, value: any) {
|
||||
this.extraInitProperties[property] = value;
|
||||
}
|
||||
|
||||
setupConnectionHandling() {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
client.on('error', logger.info);
|
||||
client.on('message', (message: string) => {
|
||||
try {
|
||||
const parsedMessage: WebsocketResponse = JSON.parse(message);
|
||||
const response = {};
|
||||
|
||||
if (parsedMessage.action === 'want') {
|
||||
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
||||
client['want-mempool-blocks'] = parsedMessage.data.indexOf('mempool-blocks') > -1;
|
||||
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
||||
}
|
||||
|
||||
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 but it might have appeared before we had the time to start watching for it
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const tx = memPool.getMempool()[client['track-tx']];
|
||||
if (tx) {
|
||||
response['tx'] = tx;
|
||||
} else {
|
||||
client['track-mempool-tx'] = parsedMessage['track-tx'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client['track-tx'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-address']) {
|
||||
if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/
|
||||
.test(parsedMessage['track-address'])) {
|
||||
client['track-address'] = parsedMessage['track-address'];
|
||||
} else {
|
||||
client['track-address'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-asset']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
||||
client['track-asset'] = parsedMessage['track-asset'];
|
||||
} else {
|
||||
client['track-asset'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'init') {
|
||||
const _blocks = blocks.getBlocks().slice(-8);
|
||||
if (!_blocks) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify(this.getInitData(_blocks)));
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'ping') {
|
||||
response['pong'] = true;
|
||||
}
|
||||
|
||||
if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
|
||||
client['track-donation'] = parsedMessage['track-donation'];
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Error parsing websocket message: ' + e.message || e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleNewDonation(id: string) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (client['track-donation'] === id) {
|
||||
client.send(JSON.stringify({ donationConfirmed: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleLoadingChanged(indicators: ILoadingIndicators) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ loadingIndicators: indicators }));
|
||||
});
|
||||
}
|
||||
|
||||
handleNewConversionRates(conversionRates: IConversionRates) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(JSON.stringify({ conversions: conversionRates }));
|
||||
});
|
||||
}
|
||||
|
||||
getInitData(_blocks?: BlockExtended[]) {
|
||||
if (!_blocks) {
|
||||
_blocks = blocks.getBlocks().slice(-8);
|
||||
}
|
||||
return {
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||
'blocks': _blocks,
|
||||
'conversions': fiatConversion.getConversionRates(),
|
||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||
'transactions': memPool.getLatestTransactions(),
|
||||
'git-commit': backendInfo.gitCommitHash,
|
||||
'hostname': backendInfo.hostname,
|
||||
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
|
||||
...this.extraInitProperties
|
||||
};
|
||||
}
|
||||
|
||||
handleNewStatistic(stats: OptimizedStatistic) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client['want-live-2h-chart']) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
handleMempoolChange(newMempool: { [txid: string]: TransactionExtended },
|
||||
newTransactions: TransactionExtended[], deletedTransactions: TransactionExtended[]) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
|
||||
this.wss.clients.forEach(async (client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {};
|
||||
|
||||
if (client['want-stats']) {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
|
||||
}
|
||||
|
||||
if (client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
}
|
||||
|
||||
if (client['track-mempool-tx']) {
|
||||
const tx = newTransactions.find((t) => t.txid === client['track-mempool-tx']);
|
||||
if (tx) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
response['tx'] = fullTx;
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
||||
}
|
||||
} else {
|
||||
response['tx'] = tx;
|
||||
}
|
||||
client['track-mempool-tx'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
for (const tx of newTransactions) {
|
||||
const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address']);
|
||||
if (someVin) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
|
||||
if (someVout) {
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
try {
|
||||
const fullTx = await transactionUtils.$getTransactionExtended(tx.txid, true);
|
||||
foundTransactions.push(fullTx);
|
||||
} catch (e) {
|
||||
logger.debug('Error finding transaction in mempool: ' + e.message || e);
|
||||
}
|
||||
} else {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
newTransactions.forEach((tx) => {
|
||||
|
||||
if (client['track-asset'] === this.nativeAssetId) {
|
||||
if (tx.vin.some((vin) => !!vin.is_pegin)) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout.some((vout) => !!vout.pegout)) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
} else {
|
||||
if (tx.vin.some((vin) => !!vin.issuance && vin.issuance.asset_id === client['track-asset'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout.some((vout) => !!vout.asset && vout.asset === client['track-asset'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
response['address-transactions'] = foundTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
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.message || e);
|
||||
}
|
||||
} else {
|
||||
response['rbfTransaction'] = rbfTx;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) {
|
||||
if (!this.wss) {
|
||||
throw new Error('WebSocket.Server is not set');
|
||||
}
|
||||
|
||||
// Check how many transactions in the new block matches the latest projected mempool block
|
||||
// If it's more than 0, recalculate the mempool blocks and send to client in the same update
|
||||
let mBlocks: undefined | MempoolBlock[];
|
||||
let matchRate = 0;
|
||||
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
if (_mempoolBlocks[0]) {
|
||||
const matches: string[] = [];
|
||||
for (const txId of txIds) {
|
||||
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
|
||||
matches.push(txId);
|
||||
}
|
||||
}
|
||||
|
||||
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
|
||||
if (matchRate > 0) {
|
||||
const currentMemPool = memPool.getMempool();
|
||||
for (const txId of matches) {
|
||||
delete currentMemPool[txId];
|
||||
}
|
||||
mempoolBlocks.updateMempoolBlocks(currentMemPool);
|
||||
mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
block.matchRate = matchRate;
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client['want-blocks']) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {
|
||||
'block': block,
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(),
|
||||
};
|
||||
|
||||
if (mBlocks && client['want-mempool-blocks']) {
|
||||
response['mempool-blocks'] = mBlocks;
|
||||
}
|
||||
|
||||
if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) {
|
||||
client['track-tx'] = null;
|
||||
response['txConfirmed'] = true;
|
||||
}
|
||||
|
||||
if (client['track-address']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_address === client['track-address'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-asset']) {
|
||||
const foundTransactions: TransactionExtended[] = [];
|
||||
|
||||
transactions.forEach((tx) => {
|
||||
if (client['track-asset'] === this.nativeAssetId) {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.is_pegin)) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => !!vout.pegout)) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
} else {
|
||||
if (tx.vin && tx.vin.some((vin) => !!vin.issuance && vin.issuance.asset_id === client['track-asset'])) {
|
||||
foundTransactions.push(tx);
|
||||
return;
|
||||
}
|
||||
if (tx.vout && tx.vout.some((vout) => !!vout.asset && vout.asset === client['track-asset'])) {
|
||||
foundTransactions.push(tx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (foundTransactions.length) {
|
||||
foundTransactions.forEach((tx) => {
|
||||
tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
});
|
||||
|
||||
response['block-transactions'] = foundTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebsocketHandler();
|
||||
@@ -1,160 +0,0 @@
|
||||
const configFile = require('../mempool-config.json');
|
||||
|
||||
interface IConfig {
|
||||
MEMPOOL: {
|
||||
NETWORK: 'mainnet' | 'testnet' | 'liquid';
|
||||
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||
HTTP_PORT: number;
|
||||
SPAWN_CLUSTER_PROCS: number;
|
||||
API_URL_PREFIX: string;
|
||||
POLL_RATE_MS: number;
|
||||
CACHE_DIR: string;
|
||||
};
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
};
|
||||
ELECTRUM: {
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
TLS_ENABLED: boolean;
|
||||
};
|
||||
CORE_RPC: {
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
};
|
||||
CORE_RPC_MINFEE: {
|
||||
ENABLED: boolean;
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
};
|
||||
DATABASE: {
|
||||
ENABLED: boolean;
|
||||
HOST: string,
|
||||
PORT: number;
|
||||
DATABASE: string;
|
||||
USERNAME: string;
|
||||
PASSWORD: string;
|
||||
};
|
||||
STATISTICS: {
|
||||
ENABLED: boolean;
|
||||
TX_PER_SECOND_SAMPLE_PERIOD: number;
|
||||
};
|
||||
BISQ_BLOCKS: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
BISQ_MARKETS: {
|
||||
ENABLED: boolean;
|
||||
DATA_PATH: string;
|
||||
};
|
||||
SPONSORS: {
|
||||
ENABLED: boolean;
|
||||
BTCPAY_URL: string;
|
||||
BTCPAY_AUTH: string;
|
||||
BTCPAY_WEBHOOK_URL: string;
|
||||
TWITTER_BEARER_AUTH: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaults: IConfig = {
|
||||
'MEMPOOL': {
|
||||
'NETWORK': 'mainnet',
|
||||
'BACKEND': 'none',
|
||||
'HTTP_PORT': 8999,
|
||||
'SPAWN_CLUSTER_PROCS': 0,
|
||||
'API_URL_PREFIX': '/api/v1/',
|
||||
'POLL_RATE_MS': 2000,
|
||||
'CACHE_DIR': './'
|
||||
},
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
},
|
||||
'ELECTRUM': {
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 3306,
|
||||
'TLS_ENABLED': true,
|
||||
},
|
||||
'CORE_RPC': {
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 8332,
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
},
|
||||
'CORE_RPC_MINFEE': {
|
||||
'ENABLED': false,
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 8332,
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
},
|
||||
'DATABASE': {
|
||||
'ENABLED': true,
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 3306,
|
||||
'DATABASE': 'mempool',
|
||||
'USERNAME': 'mempool',
|
||||
'PASSWORD': 'mempool'
|
||||
},
|
||||
'STATISTICS': {
|
||||
'ENABLED': true,
|
||||
'TX_PER_SECOND_SAMPLE_PERIOD': 150
|
||||
},
|
||||
'BISQ_BLOCKS': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db/json'
|
||||
},
|
||||
'BISQ_MARKETS': {
|
||||
'ENABLED': false,
|
||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
||||
},
|
||||
'SPONSORS': {
|
||||
'ENABLED': false,
|
||||
'BTCPAY_URL': '',
|
||||
'BTCPAY_AUTH': '',
|
||||
'BTCPAY_WEBHOOK_URL': '',
|
||||
'TWITTER_BEARER_AUTH': ''
|
||||
}
|
||||
};
|
||||
|
||||
class Config implements IConfig {
|
||||
MEMPOOL: IConfig['MEMPOOL'];
|
||||
ESPLORA: IConfig['ESPLORA'];
|
||||
ELECTRUM: IConfig['ELECTRUM'];
|
||||
CORE_RPC: IConfig['CORE_RPC'];
|
||||
CORE_RPC_MINFEE: IConfig['CORE_RPC_MINFEE'];
|
||||
DATABASE: IConfig['DATABASE'];
|
||||
STATISTICS: IConfig['STATISTICS'];
|
||||
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
|
||||
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
|
||||
SPONSORS: IConfig['SPONSORS'];
|
||||
|
||||
constructor() {
|
||||
const configs = this.merge(configFile, defaults);
|
||||
this.MEMPOOL = configs.MEMPOOL;
|
||||
this.ESPLORA = configs.ESPLORA;
|
||||
this.ELECTRUM = configs.ELECTRUM;
|
||||
this.CORE_RPC = configs.CORE_RPC;
|
||||
this.CORE_RPC_MINFEE = configs.CORE_RPC_MINFEE;
|
||||
this.DATABASE = configs.DATABASE;
|
||||
this.STATISTICS = configs.STATISTICS;
|
||||
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
|
||||
this.BISQ_MARKETS = configs.BISQ_MARKETS;
|
||||
this.SPONSORS = configs.SPONSORS;
|
||||
}
|
||||
|
||||
merge = (...objects: object[]): IConfig => {
|
||||
// @ts-ignore
|
||||
return objects.reduce((prev, next) => {
|
||||
Object.keys(prev).forEach(key => {
|
||||
next[key] = { ...next[key], ...prev[key] };
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Config();
|
||||
@@ -1,14 +1,13 @@
|
||||
import config from './config';
|
||||
const config = require('../mempool-config.json');
|
||||
import { createPool } from 'mysql2/promise';
|
||||
import logger from './logger';
|
||||
|
||||
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,
|
||||
host: config.DB_HOST,
|
||||
port: config.DB_PORT,
|
||||
database: config.DB_DATABASE,
|
||||
user: config.DB_USER,
|
||||
password: config.DB_PASSWORD,
|
||||
connectionLimit: 10,
|
||||
supportBigNumbers: true,
|
||||
});
|
||||
@@ -17,10 +16,11 @@ export class DB {
|
||||
export async function checkDbConnection() {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
logger.info('Database connection established.');
|
||||
console.log('MySQL connection established.');
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('Could not connect to database: ' + e.message || e);
|
||||
console.log('Could not connect to MySQL.');
|
||||
console.log(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +1,284 @@
|
||||
import { Express, Request, Response, NextFunction } from 'express';
|
||||
const config = require('../mempool-config.json');
|
||||
import * as fs from 'fs';
|
||||
import * as express from 'express';
|
||||
import * as compression from 'compression';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as WebSocket from 'ws';
|
||||
import * as cluster from 'cluster';
|
||||
import axios from 'axios';
|
||||
|
||||
import { checkDbConnection } from './database';
|
||||
import config from './config';
|
||||
import routes from './routes';
|
||||
import blocks from './api/blocks';
|
||||
import memPool from './api/mempool';
|
||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
||||
import diskCache from './api/disk-cache';
|
||||
import memPool from './api/mempool';
|
||||
import blocks from './api/blocks';
|
||||
import projectedBlocks from './api/projected-blocks';
|
||||
import statistics from './api/statistics';
|
||||
import websocketHandler from './api/websocket-handler';
|
||||
import fiatConversion from './api/fiat-conversion';
|
||||
import bisq from './api/bisq/bisq';
|
||||
import bisqMarkets from './api/bisq/markets';
|
||||
import donations from './api/donations';
|
||||
import logger from './logger';
|
||||
import backendInfo from './api/backend-info';
|
||||
import loadingIndicators from './api/loading-indicators';
|
||||
import mempool from './api/mempool';
|
||||
import { IBlock, IMempool, ITransaction, IMempoolStats } from './interfaces';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
private server: https.Server | http.Server | undefined;
|
||||
private app: Express;
|
||||
private currentBackendRetryInterval = 5;
|
||||
import routes from './routes';
|
||||
import fiatConversion from './api/fiat-conversion';
|
||||
|
||||
class MempoolSpace {
|
||||
private wss: WebSocket.Server;
|
||||
private server: https.Server | http.Server;
|
||||
private app: any;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
|
||||
if (!config.MEMPOOL.SPAWN_CLUSTER_PROCS) {
|
||||
this.startServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cluster.isMaster) {
|
||||
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
|
||||
for (let i = 0; i < numCPUs; i++) {
|
||||
const env = { workerId: i };
|
||||
const worker = cluster.fork(env);
|
||||
worker.process['env'] = env;
|
||||
}
|
||||
|
||||
cluster.on('exit', (worker, code, signal) => {
|
||||
const workerId = worker.process['env'].workerId;
|
||||
logger.warn(`Mempool Worker PID #${worker.process.pid} workerId: ${workerId} died. Restarting in 10 seconds... ${signal || code}`);
|
||||
setTimeout(() => {
|
||||
const env = { workerId: workerId };
|
||||
const newWorker = cluster.fork(env);
|
||||
newWorker.process['env'] = env;
|
||||
}, 10000);
|
||||
});
|
||||
} else {
|
||||
this.startServer(true);
|
||||
}
|
||||
}
|
||||
|
||||
async startServer(worker = false) {
|
||||
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||
|
||||
this.app
|
||||
.use((req: Request, res: Response, next: NextFunction) => {
|
||||
.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
next();
|
||||
})
|
||||
.use(express.urlencoded({ extended: true }))
|
||||
.use(express.json());
|
||||
|
||||
this.server = http.createServer(this.app);
|
||||
this.wss = new WebSocket.Server({ server: this.server });
|
||||
|
||||
diskCache.loadMempoolCache();
|
||||
|
||||
if (config.DATABASE.ENABLED) {
|
||||
await checkDbConnection();
|
||||
.use(compression());
|
||||
if (config.ENV === 'dev') {
|
||||
this.server = http.createServer(this.app);
|
||||
this.wss = new WebSocket.Server({ server: this.server });
|
||||
} else {
|
||||
const credentials = {
|
||||
cert: fs.readFileSync('/etc/letsencrypt/live/mempool.space/fullchain.pem'),
|
||||
key: fs.readFileSync('/etc/letsencrypt/live/mempool.space/privkey.pem'),
|
||||
};
|
||||
this.server = https.createServer(credentials, this.app);
|
||||
this.wss = new WebSocket.Server({ server: this.server });
|
||||
}
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||
statistics.startStatistics();
|
||||
}
|
||||
this.setUpRoutes();
|
||||
this.setUpWebsocketHandling();
|
||||
this.setUpMempoolCache();
|
||||
this.runMempoolIntervalFunctions();
|
||||
|
||||
statistics.startStatistics();
|
||||
fiatConversion.startService();
|
||||
|
||||
if (config.SPONSORS.ENABLED) {
|
||||
donations.$updateCache();
|
||||
}
|
||||
|
||||
this.setUpHttpApiRoutes();
|
||||
this.setUpWebsocketHandling();
|
||||
this.runMainUpdateLoop();
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
bisq.startBisqService();
|
||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
||||
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
bisqMarkets.startBisqService();
|
||||
}
|
||||
|
||||
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
||||
if (worker) {
|
||||
logger.info(`Mempool Server worker #${process.pid} started`);
|
||||
} else {
|
||||
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
||||
}
|
||||
const opts = {
|
||||
host: '127.0.0.1',
|
||||
port: 8999
|
||||
};
|
||||
this.server.listen(opts, () => {
|
||||
console.log(`Server started on ${opts.host}:${opts.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
async runMainUpdateLoop() {
|
||||
try {
|
||||
await memPool.$updateMemPoolInfo();
|
||||
await blocks.$updateBlocks();
|
||||
await memPool.$updateMempool();
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||
this.currentBackendRetryInterval = 5;
|
||||
} catch (e) {
|
||||
const loggerMsg = `runMainLoop error: ${(e.message || e)}. Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||
if (this.currentBackendRetryInterval > 5) {
|
||||
logger.warn(loggerMsg);
|
||||
mempool.setOutOfSync();
|
||||
} else {
|
||||
logger.debug(loggerMsg);
|
||||
}
|
||||
logger.debug(JSON.stringify(e));
|
||||
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
|
||||
this.currentBackendRetryInterval *= 2;
|
||||
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
|
||||
}
|
||||
private async runMempoolIntervalFunctions() {
|
||||
await blocks.updateBlocks();
|
||||
await memPool.updateMemPoolInfo();
|
||||
await memPool.updateMempool();
|
||||
setTimeout(this.runMempoolIntervalFunctions.bind(this), config.MEMPOOL_REFRESH_RATE_MS);
|
||||
}
|
||||
|
||||
setUpWebsocketHandling() {
|
||||
if (this.wss) {
|
||||
websocketHandler.setWebsocketServer(this.wss);
|
||||
private setUpMempoolCache() {
|
||||
const cacheData = diskCache.loadData();
|
||||
if (cacheData) {
|
||||
memPool.setMempool(JSON.parse(cacheData));
|
||||
}
|
||||
websocketHandler.setupConnectionHandling();
|
||||
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
|
||||
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||
memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
||||
donations.setNotfyDonationStatusCallback(websocketHandler.handleNewDonation.bind(websocketHandler));
|
||||
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
|
||||
process.on('SIGINT', (options) => {
|
||||
console.log('SIGINT');
|
||||
diskCache.saveData(JSON.stringify(memPool.getMempool()));
|
||||
process.exit(2);
|
||||
});
|
||||
}
|
||||
|
||||
setUpHttpApiRoutes() {
|
||||
private setUpWebsocketHandling() {
|
||||
this.wss.on('connection', (client: WebSocket) => {
|
||||
let theBlocks = blocks.getBlocks();
|
||||
theBlocks = theBlocks.concat([]).splice(theBlocks.length - config.INITIAL_BLOCK_AMOUNT);
|
||||
const formatedBlocks = theBlocks.map((b) => blocks.formatBlock(b));
|
||||
|
||||
client.send(JSON.stringify({
|
||||
'mempoolInfo': memPool.getMempoolInfo(),
|
||||
'blocks': formatedBlocks,
|
||||
'projectedBlocks': projectedBlocks.getProjectedBlocks(),
|
||||
'txPerSecond': memPool.getTxPerSecond(),
|
||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||
'conversions': fiatConversion.getTickers()['BTCUSD'],
|
||||
}));
|
||||
|
||||
client.on('message', async (message: any) => {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
|
||||
if (parsedMessage.action === 'want') {
|
||||
client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
|
||||
client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
|
||||
client['want-projected-blocks'] = parsedMessage.data.indexOf('projected-blocks') > -1;
|
||||
client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
|
||||
}
|
||||
|
||||
if (parsedMessage.action === 'track-tx' && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
|
||||
const tx = await memPool.getRawTransaction(parsedMessage.txId);
|
||||
if (tx) {
|
||||
console.log('Now tracking: ' + parsedMessage.txId);
|
||||
client['trackingTx'] = true;
|
||||
client['txId'] = parsedMessage.txId;
|
||||
client['tx'] = tx;
|
||||
|
||||
if (tx.blockhash) {
|
||||
const currentBlocks = blocks.getBlocks();
|
||||
const foundBlock = currentBlocks.find((block) => block.tx && block.tx.some((i: string) => i === parsedMessage.txId));
|
||||
if (foundBlock) {
|
||||
console.log('Found block by looking in local cache');
|
||||
client['blockHeight'] = foundBlock.height;
|
||||
} else {
|
||||
const theBlock = await bitcoinApi.getBlockAndTransactions(tx.blockhash);
|
||||
if (theBlock) {
|
||||
client['blockHeight'] = theBlock.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client['blockHeight'] = 0;
|
||||
}
|
||||
client.send(JSON.stringify({
|
||||
'projectedBlocks': projectedBlocks.getProjectedBlocks(client['txId']),
|
||||
'track-tx': {
|
||||
tracking: true,
|
||||
blockHeight: client['blockHeight'],
|
||||
tx: client['tx'],
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
console.log('TX NOT FOUND, NOT TRACKING');
|
||||
client['trackingTx'] = false;
|
||||
client['blockHeight'] = 0;
|
||||
client['tx'] = null;
|
||||
client.send(JSON.stringify({
|
||||
'track-tx': {
|
||||
tracking: false,
|
||||
blockHeight: 0,
|
||||
message: 'not-found',
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (parsedMessage.action === 'stop-tracking-tx') {
|
||||
console.log('STOP TRACKING');
|
||||
client['trackingTx'] = false;
|
||||
client.send(JSON.stringify({
|
||||
'track-tx': {
|
||||
tracking: false,
|
||||
blockHeight: 0,
|
||||
message: 'not-found',
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
client['trackingTx'] = false;
|
||||
});
|
||||
});
|
||||
|
||||
blocks.setNewBlockCallback((block: IBlock) => {
|
||||
const formattedBlocks = blocks.formatBlock(block);
|
||||
|
||||
this.wss.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {};
|
||||
|
||||
if (client['trackingTx'] === true && client['blockHeight'] === 0) {
|
||||
if (block.tx.some((tx: ITransaction) => tx === client['txId'])) {
|
||||
client['blockHeight'] = block.height;
|
||||
}
|
||||
}
|
||||
|
||||
response['track-tx'] = {
|
||||
tracking: client['trackingTx'] || false,
|
||||
blockHeight: client['blockHeight'],
|
||||
};
|
||||
|
||||
response['block'] = formattedBlocks;
|
||||
|
||||
client.send(JSON.stringify(response));
|
||||
});
|
||||
});
|
||||
|
||||
memPool.setMempoolChangedCallback((newMempool: IMempool) => {
|
||||
projectedBlocks.updateProjectedBlocks(newMempool);
|
||||
|
||||
const pBlocks = projectedBlocks.getProjectedBlocks();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const txPerSecond = memPool.getTxPerSecond();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {};
|
||||
|
||||
if (client['want-stats']) {
|
||||
response['mempoolInfo'] = mempoolInfo;
|
||||
response['txPerSecond'] = txPerSecond;
|
||||
response['vBytesPerSecond'] = vBytesPerSecond;
|
||||
response['track-tx'] = {
|
||||
tracking: client['trackingTx'] || false,
|
||||
blockHeight: client['blockHeight'],
|
||||
};
|
||||
}
|
||||
|
||||
if (client['want-projected-blocks'] && client['trackingTx'] && client['blockHeight'] === 0) {
|
||||
response['projectedBlocks'] = projectedBlocks.getProjectedBlocks(client['txId']);
|
||||
} else if (client['want-projected-blocks']) {
|
||||
response['projectedBlocks'] = pBlocks;
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
statistics.setNewStatisticsEntryCallback((stats: IMempoolStats) => {
|
||||
this.wss.clients.forEach((client: WebSocket) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client['want-live-2h-chart']) {
|
||||
client.send(JSON.stringify({
|
||||
'live-2h-chart': stats
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setUpRoutes() {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
|
||||
;
|
||||
.get(config.API_ENDPOINT + 'transactions/height/:id', routes.$getgetTransactionsForBlock)
|
||||
.get(config.API_ENDPOINT + 'transactions/projected/:id', routes.getgetTransactionsForProjectedBlock)
|
||||
.get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
|
||||
.get(config.API_ENDPOINT + 'fees/projected-blocks', routes.getProjectedBlocks)
|
||||
.get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
|
||||
.get(config.API_ENDPOINT + 'statistics/24h', routes.get24HStatistics.bind(routes))
|
||||
.get(config.API_ENDPOINT + 'statistics/1w', routes.get1WHStatistics.bind(routes))
|
||||
.get(config.API_ENDPOINT + 'statistics/1m', routes.get1MStatistics.bind(routes))
|
||||
.get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
|
||||
.get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
|
||||
;
|
||||
|
||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||
if (config.BACKEND_API === 'electrs') {
|
||||
this.app
|
||||
.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.API_ENDPOINT + 'explorer/blocks', routes.getBlocks)
|
||||
.get(config.API_ENDPOINT + 'explorer/blocks/:height', routes.getBlocks)
|
||||
.get(config.API_ENDPOINT + 'explorer/tx/:id', routes.getRawTransaction)
|
||||
.get(config.API_ENDPOINT + 'explorer/block/:hash', routes.getBlock)
|
||||
.get(config.API_ENDPOINT + 'explorer/block/:hash/tx', routes.getBlockTransactions)
|
||||
.get(config.API_ENDPOINT + 'explorer/block/:hash/tx/:index', routes.getBlockTransactionsFromIndex)
|
||||
.get(config.API_ENDPOINT + 'explorer/address/:address', routes.getAddress)
|
||||
.get(config.API_ENDPOINT + 'explorer/address/:address/tx', routes.getAddressTransactions)
|
||||
.get(config.API_ENDPOINT + 'explorer/address/:address/tx/chain/:txid', routes.getAddressTransactionsFromTxid)
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/stats', routes.getBisqStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/tx/:txId', routes.getBisqTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/block/:hash', routes.getBisqBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/tip/height', routes.getBisqTip)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/address/:address', routes.getBisqAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/txs/:index/:length', routes.getBisqTransactions)
|
||||
;
|
||||
}
|
||||
|
||||
if (config.BISQ_MARKETS.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/currencies', routes.getBisqMarketCurrencies.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/depth', routes.getBisqMarketDepth.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/hloc', routes.getBisqMarketHloc.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/markets', routes.getBisqMarketMarkets.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/offers', routes.getBisqMarketOffers.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/ticker', routes.getBisqMarketTicker.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/trades', routes.getBisqMarketTrades.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
|
||||
;
|
||||
}
|
||||
|
||||
if (config.SPONSORS.ENABLED) {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.getDonations.bind(routes))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', routes.getSponsorImage.bind(routes))
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.createDonationRequest.bind(routes))
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'donations-webhook', routes.donationWebhook.bind(routes))
|
||||
;
|
||||
} else {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
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();
|
||||
}
|
||||
})
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
|
||||
responseType: 'stream', timeout: 10000
|
||||
});
|
||||
response.data.pipe(res);
|
||||
} catch (e) {
|
||||
res.status(500).end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||
this.app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/txids', routes.getMempoolTxIds)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', routes.getRecentMempoolTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
|
||||
.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 + '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)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', routes.getBlockHeight)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new Server();
|
||||
const mempoolSpace = new MempoolSpace();
|
||||
|
||||
152
backend/src/interfaces.ts
Normal file
152
backend/src/interfaces.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
export interface IMempoolInfo {
|
||||
size: number;
|
||||
bytes: number;
|
||||
usage?: number;
|
||||
maxmempool?: number;
|
||||
mempoolminfee?: number;
|
||||
minrelaytxfee?: number;
|
||||
}
|
||||
|
||||
export interface ITransaction {
|
||||
txid: string;
|
||||
hash: string;
|
||||
version: number;
|
||||
size: number;
|
||||
vsize: number;
|
||||
weight: number;
|
||||
locktime: number;
|
||||
vin: Vin[];
|
||||
vout: Vout[];
|
||||
hex: string;
|
||||
fee: number;
|
||||
feePerWeightUnit: number;
|
||||
feePerVsize: number;
|
||||
blockhash?: string;
|
||||
confirmations?: number;
|
||||
time?: number;
|
||||
blocktime?: number;
|
||||
totalOut?: number;
|
||||
}
|
||||
|
||||
export interface IBlock {
|
||||
hash: string;
|
||||
confirmations: number;
|
||||
strippedsize: number;
|
||||
size: number;
|
||||
weight: number;
|
||||
height: number;
|
||||
version: number;
|
||||
versionHex: string;
|
||||
merkleroot: string;
|
||||
tx: any;
|
||||
time: number;
|
||||
mediantime: number;
|
||||
nonce: number;
|
||||
bits: string;
|
||||
difficulty: number;
|
||||
chainwork: string;
|
||||
nTx: number;
|
||||
previousblockhash: string;
|
||||
fees: number;
|
||||
|
||||
minFee?: number;
|
||||
maxFee?: number;
|
||||
medianFee?: number;
|
||||
}
|
||||
|
||||
interface ScriptSig {
|
||||
asm: string;
|
||||
hex: string;
|
||||
}
|
||||
|
||||
interface Vin {
|
||||
txid: string;
|
||||
vout: number;
|
||||
scriptSig: ScriptSig;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
interface ScriptPubKey {
|
||||
asm: string;
|
||||
hex: string;
|
||||
reqSigs: number;
|
||||
type: string;
|
||||
addresses: string[];
|
||||
}
|
||||
|
||||
interface Vout {
|
||||
value: number;
|
||||
n: number;
|
||||
scriptPubKey: ScriptPubKey;
|
||||
}
|
||||
|
||||
export interface IMempoolStats {
|
||||
id?: number;
|
||||
added: string;
|
||||
unconfirmed_transactions: number;
|
||||
tx_per_second: number;
|
||||
vbytes_per_second: number;
|
||||
total_fee: number;
|
||||
mempool_byte_weight: number;
|
||||
fee_data: string;
|
||||
|
||||
vsize_1: number;
|
||||
vsize_2: number;
|
||||
vsize_3: number;
|
||||
vsize_4: number;
|
||||
vsize_5: number;
|
||||
vsize_6: number;
|
||||
vsize_8: number;
|
||||
vsize_10: number;
|
||||
vsize_12: number;
|
||||
vsize_15: number;
|
||||
vsize_20: number;
|
||||
vsize_30: number;
|
||||
vsize_40: number;
|
||||
vsize_50: number;
|
||||
vsize_60: number;
|
||||
vsize_70: number;
|
||||
vsize_80: number;
|
||||
vsize_90: number;
|
||||
vsize_100: number;
|
||||
vsize_125: number;
|
||||
vsize_150: number;
|
||||
vsize_175: number;
|
||||
vsize_200: number;
|
||||
vsize_250: number;
|
||||
vsize_300: number;
|
||||
vsize_350: number;
|
||||
vsize_400: number;
|
||||
vsize_500: number;
|
||||
vsize_600: number;
|
||||
vsize_700: number;
|
||||
vsize_800: number;
|
||||
vsize_900: number;
|
||||
vsize_1000: number;
|
||||
vsize_1200: number;
|
||||
vsize_1400: number;
|
||||
vsize_1600: number;
|
||||
vsize_1800: number;
|
||||
vsize_2000: number;
|
||||
}
|
||||
|
||||
export interface IProjectedBlockInternal extends IProjectedBlock {
|
||||
txIds: string[];
|
||||
txFeePerVsizes: number[];
|
||||
}
|
||||
|
||||
export interface IProjectedBlock {
|
||||
blockSize: number;
|
||||
blockWeight: number;
|
||||
maxFee: number;
|
||||
maxWeightFee: number;
|
||||
medianFee: number;
|
||||
minFee: number;
|
||||
minWeightFee: number;
|
||||
nTx: number;
|
||||
fees: number;
|
||||
hasMyTxId?: boolean;
|
||||
}
|
||||
|
||||
export interface IMempool { [txid: string]: ITransaction; }
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import config from './config';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
class Logger {
|
||||
static priorities = {
|
||||
emerg: 0,
|
||||
alert: 1,
|
||||
crit: 2,
|
||||
err: 3,
|
||||
warn: 4,
|
||||
notice: 5,
|
||||
info: 6,
|
||||
debug: 7
|
||||
};
|
||||
static facilities = {
|
||||
kern: 0,
|
||||
user: 1,
|
||||
mail: 2,
|
||||
daemon: 3,
|
||||
auth: 4,
|
||||
syslog: 5,
|
||||
lpr: 6,
|
||||
news: 7,
|
||||
uucp: 8,
|
||||
local0: 16,
|
||||
local1: 17,
|
||||
local2: 18,
|
||||
local3: 19,
|
||||
local4: 20,
|
||||
local5: 21,
|
||||
local6: 22,
|
||||
local7: 23
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
public emerg: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public alert: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public crit: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public err: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public warn: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public notice: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public info: ((msg: string) => void);
|
||||
// @ts-ignore
|
||||
public debug: ((msg: string) => void);
|
||||
|
||||
private name = 'mempool';
|
||||
private fac: any;
|
||||
private loghost: string;
|
||||
private logport: number;
|
||||
private client: dgram.Socket;
|
||||
private network: string;
|
||||
|
||||
constructor(fac) {
|
||||
let prio;
|
||||
this.fac = fac != null ? fac : Logger.facilities.local0;
|
||||
this.loghost = '127.0.0.1';
|
||||
this.logport = 514;
|
||||
for (prio in Logger.priorities) {
|
||||
if (true) {
|
||||
this.addprio(prio);
|
||||
}
|
||||
}
|
||||
this.client = dgram.createSocket('udp4');
|
||||
this.network = this.getNetwork();
|
||||
}
|
||||
|
||||
private addprio(prio): void {
|
||||
this[prio] = (function(_this) {
|
||||
return function(msg) {
|
||||
return _this.msg(prio, msg);
|
||||
};
|
||||
})(this);
|
||||
}
|
||||
|
||||
private getNetwork(): string {
|
||||
if (config.BISQ_BLOCKS.ENABLED) {
|
||||
return 'bisq';
|
||||
}
|
||||
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
|
||||
return config.MEMPOOL.NETWORK;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private msg(priority, msg) {
|
||||
let consolemsg, prionum, syslogmsg;
|
||||
if (typeof msg === 'string' && msg.length > 0) {
|
||||
while (msg[msg.length - 1].charCodeAt(0) === 10) {
|
||||
msg = msg.slice(0, msg.length - 1);
|
||||
}
|
||||
}
|
||||
const network = this.network ? ' <' + this.network + '>' : '';
|
||||
prionum = Logger.priorities[priority] || Logger.priorities.info;
|
||||
syslogmsg = `<${(this.fac * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
|
||||
consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`;
|
||||
|
||||
this.syslog(syslogmsg);
|
||||
if (priority === 'warning') {
|
||||
priority = 'warn';
|
||||
}
|
||||
if (priority === 'debug') {
|
||||
priority = 'info';
|
||||
}
|
||||
if (priority === 'err') {
|
||||
priority = 'error';
|
||||
}
|
||||
return (console[priority] || console.error)(consolemsg);
|
||||
}
|
||||
|
||||
private syslog(msg) {
|
||||
let msgbuf;
|
||||
msgbuf = Buffer.from(msg);
|
||||
this.client.send(msgbuf, 0, msgbuf.length, this.logport, this.loghost, function(err, bytes) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private leadZero(n: number): number | string {
|
||||
if (n < 10) {
|
||||
return '0' + n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private ts() {
|
||||
let day, dt, hours, minutes, month, months, seconds;
|
||||
dt = new Date();
|
||||
hours = this.leadZero(dt.getHours());
|
||||
minutes = this.leadZero(dt.getMinutes());
|
||||
seconds = this.leadZero(dt.getSeconds());
|
||||
month = dt.getMonth();
|
||||
day = dt.getDate();
|
||||
if (day < 10) {
|
||||
day = ' ' + day;
|
||||
}
|
||||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger(Logger.facilities.local7);
|
||||
@@ -1,140 +0,0 @@
|
||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
||||
|
||||
export interface MempoolBlock {
|
||||
blockSize: number;
|
||||
blockVSize: number;
|
||||
nTx: number;
|
||||
medianFee: number;
|
||||
totalFees: number;
|
||||
feeRange: number[];
|
||||
}
|
||||
|
||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
|
||||
transactionIds: string[];
|
||||
}
|
||||
|
||||
interface VinStrippedToScriptsig {
|
||||
scriptsig: string;
|
||||
}
|
||||
|
||||
interface VoutStrippedToScriptPubkey {
|
||||
scriptpubkey_address: string | undefined;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
vsize: number;
|
||||
feePerVsize: number;
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface TransactionStripped {
|
||||
txid: string;
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
}
|
||||
export interface BlockExtended extends IEsploraApi.Block {
|
||||
medianFee?: number;
|
||||
feeRange?: number[];
|
||||
reward?: number;
|
||||
coinbaseTx?: TransactionMinerInfo;
|
||||
matchRate?: number;
|
||||
}
|
||||
|
||||
export interface TransactionMinerInfo {
|
||||
vin: VinStrippedToScriptsig[];
|
||||
vout: VoutStrippedToScriptPubkey[];
|
||||
}
|
||||
|
||||
export interface MempoolStats {
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
id?: number;
|
||||
added: string;
|
||||
unconfirmed_transactions: number;
|
||||
tx_per_second: number;
|
||||
vbytes_per_second: number;
|
||||
total_fee: number;
|
||||
mempool_byte_weight: number;
|
||||
fee_data: string;
|
||||
|
||||
vsize_1: number;
|
||||
vsize_2: number;
|
||||
vsize_3: number;
|
||||
vsize_4: number;
|
||||
vsize_5: number;
|
||||
vsize_6: number;
|
||||
vsize_8: number;
|
||||
vsize_10: number;
|
||||
vsize_12: number;
|
||||
vsize_15: number;
|
||||
vsize_20: number;
|
||||
vsize_30: number;
|
||||
vsize_40: number;
|
||||
vsize_50: number;
|
||||
vsize_60: number;
|
||||
vsize_70: number;
|
||||
vsize_80: number;
|
||||
vsize_90: number;
|
||||
vsize_100: number;
|
||||
vsize_125: number;
|
||||
vsize_150: number;
|
||||
vsize_175: number;
|
||||
vsize_200: number;
|
||||
vsize_250: number;
|
||||
vsize_300: number;
|
||||
vsize_350: number;
|
||||
vsize_400: number;
|
||||
vsize_500: number;
|
||||
vsize_600: number;
|
||||
vsize_700: number;
|
||||
vsize_800: number;
|
||||
vsize_900: number;
|
||||
vsize_1000: number;
|
||||
vsize_1200: number;
|
||||
vsize_1400: number;
|
||||
vsize_1600: number;
|
||||
vsize_1800: number;
|
||||
vsize_2000: number;
|
||||
}
|
||||
|
||||
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;
|
||||
vsizes: number[];
|
||||
}
|
||||
|
||||
export interface WebsocketResponse {
|
||||
action: string;
|
||||
data: string[];
|
||||
'track-tx': string;
|
||||
'track-address': string;
|
||||
'watch-mempool': boolean;
|
||||
}
|
||||
|
||||
export interface VbytesPerSecond {
|
||||
unixTime: number;
|
||||
vSize: number;
|
||||
}
|
||||
|
||||
export interface RequiredSpec { [name: string]: RequiredParams; }
|
||||
|
||||
interface RequiredParams {
|
||||
required: boolean;
|
||||
types: ('@string' | '@number' | '@boolean' | string)[];
|
||||
}
|
||||
|
||||
export interface ILoadingIndicators { [name: string]: number; }
|
||||
export interface IConversionRates { [currency: string]: number; }
|
||||
@@ -1,711 +1,198 @@
|
||||
import config from './config';
|
||||
import { Request, Response } from 'express';
|
||||
import statistics from './api/statistics';
|
||||
import feeApi from './api/fee-api';
|
||||
import backendInfo from './api/backend-info';
|
||||
import mempoolBlocks from './api/mempool-blocks';
|
||||
import mempool from './api/mempool';
|
||||
import bisq from './api/bisq/bisq';
|
||||
import websocketHandler from './api/websocket-handler';
|
||||
import bisqMarket from './api/bisq/markets-api';
|
||||
import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.interfaces';
|
||||
import { MarketsApiError } from './api/bisq/interfaces';
|
||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
|
||||
import donations from './api/donations';
|
||||
import logger from './logger';
|
||||
import projectedBlocks from './api/projected-blocks';
|
||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
||||
import transactionUtils from './api/transaction-utils';
|
||||
import blocks from './api/blocks';
|
||||
import loadingIndicators from './api/loading-indicators';
|
||||
import { Common } from './api/common';
|
||||
|
||||
class Routes {
|
||||
constructor() {}
|
||||
private cache = {};
|
||||
|
||||
public async get2HStatistics(req: Request, res: Response) {
|
||||
constructor() {
|
||||
this.createCache();
|
||||
setInterval(this.createCache.bind(this), 600000);
|
||||
}
|
||||
|
||||
private async createCache() {
|
||||
this.cache['24h'] = await statistics.$list24H();
|
||||
this.cache['1w'] = await statistics.$list1W();
|
||||
this.cache['1m'] = await statistics.$list1M();
|
||||
this.cache['3m'] = await statistics.$list3M();
|
||||
this.cache['6m'] = await statistics.$list6M();
|
||||
console.log('Statistics cache created');
|
||||
}
|
||||
|
||||
public async get2HStatistics(req, res) {
|
||||
const result = await statistics.$list2H();
|
||||
res.json(result);
|
||||
res.send(result);
|
||||
}
|
||||
|
||||
public get24HStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['24h']);
|
||||
public get24HStatistics(req, res) {
|
||||
res.send(this.cache['24h']);
|
||||
}
|
||||
|
||||
public get1WHStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['1w']);
|
||||
public get1WHStatistics(req, res) {
|
||||
res.send(this.cache['1w']);
|
||||
}
|
||||
|
||||
public get1MStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['1m']);
|
||||
public get1MStatistics(req, res) {
|
||||
res.send(this.cache['1m']);
|
||||
}
|
||||
|
||||
public get3MStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['3m']);
|
||||
public get3MStatistics(req, res) {
|
||||
res.send(this.cache['3m']);
|
||||
}
|
||||
|
||||
public get6MStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['6m']);
|
||||
public get6MStatistics(req, res) {
|
||||
res.send(this.cache['6m']);
|
||||
}
|
||||
|
||||
public get1YStatistics(req: Request, res: Response) {
|
||||
res.json(statistics.getCache()['1y']);
|
||||
}
|
||||
|
||||
public getInitData(req: Request, res: Response) {
|
||||
try {
|
||||
const result = websocketHandler.getInitData();
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRecommendedFees(req: Request, res: Response) {
|
||||
if (!mempool.isInSync()) {
|
||||
res.statusCode = 503;
|
||||
res.send('Service Unavailable');
|
||||
return;
|
||||
}
|
||||
public async getRecommendedFees(req, res) {
|
||||
const result = feeApi.getRecommendedFee();
|
||||
res.json(result);
|
||||
res.send(result);
|
||||
}
|
||||
|
||||
public getMempoolBlocks(req: Request, res: Response) {
|
||||
public async $getgetTransactionsForBlock(req, res) {
|
||||
const result = await feeApi.$getTransactionsForBlock(req.params.id);
|
||||
res.send(result);
|
||||
}
|
||||
|
||||
public async getgetTransactionsForProjectedBlock(req, res) {
|
||||
try {
|
||||
const result = mempoolBlocks.getMempoolBlocks();
|
||||
res.json(result);
|
||||
const result = await projectedBlocks.getProjectedBlockFeesForBlock(req.params.id);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public getTransactionTimes(req: Request, res: Response) {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(500).send('Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
for (const _txId in req.query.txId) {
|
||||
if (typeof req.query.txId[_txId] === 'string') {
|
||||
txIds.push(req.query.txId[_txId].toString());
|
||||
public async getProjectedBlocks(req, res) {
|
||||
try {
|
||||
let txId: string | undefined;
|
||||
if (req.query.txId && /^[a-fA-F0-9]{64}$/.test(req.query.txId)) {
|
||||
txId = req.query.txId;
|
||||
}
|
||||
}
|
||||
|
||||
const times = mempool.getFirstSeenForTransactions(txIds);
|
||||
res.json(times);
|
||||
}
|
||||
|
||||
public getBackendInfo(req: Request, res: Response) {
|
||||
res.json(backendInfo.getBackendInfo());
|
||||
}
|
||||
|
||||
public async createDonationRequest(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'amount': {
|
||||
required: true,
|
||||
types: ['@float']
|
||||
},
|
||||
'orderId': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
}
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.body, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).send(p.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.orderId !== '' && !/^(@|)[a-zA-Z0-9_]{1,15}$/.test(p.orderId)) {
|
||||
res.status(400).send('Invalid Twitter handle');
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.amount < 0.001) {
|
||||
res.status(400).send('Amount needs to be at least 0.001');
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.amount > 1000) {
|
||||
res.status(400).send('Amount too large');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await donations.$createRequest(p.amount, p.orderId);
|
||||
res.json(result);
|
||||
const result = await projectedBlocks.getProjectedBlocks(txId, 6);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async getDonations(req: Request, res: Response) {
|
||||
public async getBlocks(req, res) {
|
||||
try {
|
||||
const result = await donations.$getDonationsFromDatabase('handle, imageUrl');
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSponsorImage(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await donations.getSponsorImage(req.params.id);
|
||||
if (result) {
|
||||
res.set('Content-Type', 'image/jpeg');
|
||||
res.send(result);
|
||||
let result: string;
|
||||
if (req.params.height) {
|
||||
result = await bitcoinApi.getBlocksFromHeight(req.params.height);
|
||||
} else {
|
||||
res.status(404).end();
|
||||
result = await bitcoinApi.getBlocks();
|
||||
}
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public async donationWebhook(req: Request, res: Response) {
|
||||
public async getRawTransaction(req, res) {
|
||||
try {
|
||||
donations.$handleWebhookRequest(req.body);
|
||||
res.end();
|
||||
const result = await bitcoinApi.getRawTransaction(req.params.id);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e);
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqStats(req: Request, res: Response) {
|
||||
const result = bisq.getStats();
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
public getBisqTip(req: Request, res: Response) {
|
||||
const result = bisq.getLatestBlockHeight();
|
||||
res.type('text/plain');
|
||||
res.send(result.toString());
|
||||
}
|
||||
|
||||
public getBisqTransaction(req: Request, res: Response) {
|
||||
const result = bisq.getTransaction(req.params.txId);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq transaction not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqTransactions(req: Request, res: Response) {
|
||||
const types: string[] = [];
|
||||
req.query.types = req.query.types || [];
|
||||
if (!Array.isArray(req.query.types)) {
|
||||
res.status(500).send('Types is not an array');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const _type in req.query.types) {
|
||||
if (typeof req.query.types[_type] === 'string') {
|
||||
types.push(req.query.types[_type].toString());
|
||||
}
|
||||
}
|
||||
|
||||
const index = parseInt(req.params.index, 10) || 0;
|
||||
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
|
||||
const [transactions, count] = bisq.getTransactions(index, length, types);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
public getBisqBlock(req: Request, res: Response) {
|
||||
const result = bisq.getBlock(req.params.hash);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq block not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqBlocks(req: Request, res: Response) {
|
||||
const index = parseInt(req.params.index, 10) || 0;
|
||||
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
|
||||
const [transactions, count] = bisq.getBlocks(index, length);
|
||||
res.header('X-Total-Count', count.toString());
|
||||
res.json(transactions);
|
||||
}
|
||||
|
||||
public getBisqAddress(req: Request, res: Response) {
|
||||
const result = bisq.getAddress(req.params.address.substr(1));
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('Bisq address not found');
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketCurrencies(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'type': {
|
||||
required: false,
|
||||
types: ['crypto', 'fiat', 'all']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getCurrencies(p.type);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketCurrencies error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketDepth(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getDepth(p.market);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketDepth error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketMarkets(req: Request, res: Response) {
|
||||
const result = bisqMarket.getMarkets();
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketMarkets error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketTrades(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'trade_id_to': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'trade_id_from': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'direction': {
|
||||
required: false,
|
||||
types: ['buy', 'sell']
|
||||
},
|
||||
'limit': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'sort': {
|
||||
required: false,
|
||||
types: ['asc', 'desc']
|
||||
}
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getTrades(p.market, p.timestamp_from,
|
||||
p.timestamp_to, p.trade_id_from, p.trade_id_to, p.direction, p.limit, p.sort);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTrades error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketOffers(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'direction': {
|
||||
required: false,
|
||||
types: ['buy', 'sell']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getOffers(p.market, p.direction);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketOffers error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketVolumes(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
'interval': {
|
||||
required: false,
|
||||
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'milliseconds': {
|
||||
required: false,
|
||||
types: ['@boolean']
|
||||
},
|
||||
'timestamp': {
|
||||
required: false,
|
||||
types: ['no', 'yes']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getVolumes(p.market, p.timestamp_from, p.timestamp_to, p.interval, p.milliseconds, p.timestamp);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketVolumes error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketHloc(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: true,
|
||||
types: ['@string']
|
||||
},
|
||||
'interval': {
|
||||
required: false,
|
||||
types: ['minute', 'half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto']
|
||||
},
|
||||
'timestamp_from': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'timestamp_to': {
|
||||
required: false,
|
||||
types: ['@number']
|
||||
},
|
||||
'milliseconds': {
|
||||
required: false,
|
||||
types: ['@boolean']
|
||||
},
|
||||
'timestamp': {
|
||||
required: false,
|
||||
types: ['no', 'yes']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getHloc(p.market, p.interval, p.timestamp_from, p.timestamp_to, p.milliseconds, p.timestamp);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketHloc error'));
|
||||
}
|
||||
}
|
||||
|
||||
public getBisqMarketTicker(req: Request, res: Response) {
|
||||
const constraints: RequiredSpec = {
|
||||
'market': {
|
||||
required: false,
|
||||
types: ['@string']
|
||||
},
|
||||
};
|
||||
|
||||
const p = this.parseRequestParameters(req.query, constraints);
|
||||
if (p.error) {
|
||||
res.status(400).json(this.getBisqMarketErrorResponse(p.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = bisqMarket.getTicker(p.market);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(500).json(this.getBisqMarketErrorResponse('getBisqMarketTicker error'));
|
||||
}
|
||||
}
|
||||
|
||||
private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
|
||||
const final = {};
|
||||
for (const i in params) {
|
||||
if (params.hasOwnProperty(i)) {
|
||||
if (params[i].required && requestParams[i] === undefined) {
|
||||
return { error: i + ' parameter missing'};
|
||||
}
|
||||
if (typeof requestParams[i] === 'string') {
|
||||
const str = (requestParams[i] || '').toString().toLowerCase();
|
||||
if (params[i].types.indexOf('@number') > -1) {
|
||||
const number = parseInt((str).toString(), 10);
|
||||
final[i] = number;
|
||||
} else if (params[i].types.indexOf('@string') > -1) {
|
||||
final[i] = str;
|
||||
} else if (params[i].types.indexOf('@boolean') > -1) {
|
||||
final[i] = str === 'true' || str === 'yes';
|
||||
} else if (params[i].types.indexOf(str) > -1) {
|
||||
final[i] = str;
|
||||
} else {
|
||||
return { error: i + ' parameter invalid'};
|
||||
}
|
||||
} else if (typeof requestParams[i] === 'number') {
|
||||
final[i] = requestParams[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return final;
|
||||
}
|
||||
|
||||
private getBisqMarketErrorResponse(message: string): MarketsApiError {
|
||||
return {
|
||||
'success': 0,
|
||||
'error': message
|
||||
};
|
||||
}
|
||||
|
||||
public async getTransaction(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||
res.json(transaction);
|
||||
} catch (e) {
|
||||
let statusCode = 500;
|
||||
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTransactionStatus(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
|
||||
res.json(transaction.status);
|
||||
} catch (e) {
|
||||
let statusCode = 500;
|
||||
if (e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
|
||||
statusCode = 404;
|
||||
}
|
||||
res.status(statusCode).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlock(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocks', 0);
|
||||
|
||||
const returnBlocks: IEsploraApi.Block[] = [];
|
||||
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
|
||||
|
||||
// Check if block height exist in local cache to skip the hash lookup
|
||||
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
|
||||
let startFromHash: string | null = null;
|
||||
if (blockByHeight) {
|
||||
startFromHash = blockByHeight.id;
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
startFromHash = await bitcoinApi.$getBlockHash(fromHeight);
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nextHash = startFromHash;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const localBlock = blocks.getBlocks().find((b) => b.id === nextHash);
|
||||
if (localBlock) {
|
||||
returnBlocks.push(localBlock);
|
||||
nextHash = localBlock.previousblockhash;
|
||||
public async getBlock(req, res) {
|
||||
try {
|
||||
const result = await bitcoinApi.getBlock(req.params.hash);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlockTransactions(req, res) {
|
||||
try {
|
||||
const result = await bitcoinApi.getBlockTransactions(req.params.hash);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
const block = await bitcoinApi.$getBlock(nextHash);
|
||||
returnBlocks.push(block);
|
||||
nextHash = block.previousblockhash;
|
||||
}
|
||||
loadingIndicators.setProgress('blocks', i / 10 * 100);
|
||||
}
|
||||
|
||||
res.json(returnBlocks);
|
||||
} catch (e) {
|
||||
loadingIndicators.setProgress('blocks', 100);
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlockTransactions(req: Request, res: Response) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||
|
||||
const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||
const transactions: TransactionExtended[] = [];
|
||||
const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10));
|
||||
|
||||
const endIndex = Math.min(startingIndex + 10, txIds.length);
|
||||
for (let i = startingIndex; i < endIndex; i++) {
|
||||
try {
|
||||
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true);
|
||||
transactions.push(transaction);
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
||||
} catch (e) {
|
||||
logger.debug('getBlockTransactions error: ' + e.message || e);
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlockHeight(req: Request, res: Response) {
|
||||
public async getBlockTransactionsFromIndex(req, res) {
|
||||
try {
|
||||
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
|
||||
res.send(blockHash);
|
||||
const result = await bitcoinApi.getBlockTransactionsFromIndex(req.params.hash, req.params.index);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAddress(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const addressData = await bitcoinApi.$getAddress(req.params.address);
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
if (e.message && e.message.indexOf('exceeds') > 0) {
|
||||
return res.status(413).send(e.message);
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAddressTransactions(req: Request, res: Response) {
|
||||
if (config.MEMPOOL.BACKEND === 'none') {
|
||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||
return;
|
||||
}
|
||||
|
||||
public async getAddress(req, res) {
|
||||
try {
|
||||
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
||||
res.json(transactions);
|
||||
const result = await bitcoinApi.getAddress(req.params.address);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
if (e.message && e.message.indexOf('exceeds') > 0) {
|
||||
return res.status(413).send(e.message);
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAdressTxChain(req: Request, res: Response) {
|
||||
res.status(501).send('Not implemented');
|
||||
}
|
||||
|
||||
public async getAddressPrefix(req: Request, res: Response) {
|
||||
public async getAddressTransactions(req, res) {
|
||||
try {
|
||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
res.send(blockHash);
|
||||
const result = await bitcoinApi.getAddressTransactions(req.params.address);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getRecentMempoolTransactions(req: Request, res: Response) {
|
||||
const latestTransactions = Object.entries(mempool.getMempool())
|
||||
.sort((a, b) => (b[1].firstSeen || 0) - (a[1].firstSeen || 0))
|
||||
.slice(0, 10).map((tx) => Common.stripTransaction(tx[1]));
|
||||
|
||||
res.json(latestTransactions);
|
||||
}
|
||||
|
||||
public async getMempool(req: Request, res: Response) {
|
||||
res.status(501).send('Not implemented');
|
||||
}
|
||||
|
||||
public async getMempoolTxIds(req: Request, res: Response) {
|
||||
public async getAddressTransactionsFromTxid(req, res) {
|
||||
try {
|
||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||
res.send(rawMempool);
|
||||
const result = await bitcoinApi.getAddressTransactionsFromLastSeenTxid(req.params.address, req.params.txid);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
if (e.response) {
|
||||
res.status(e.response.status).send(e.response.data);
|
||||
} else {
|
||||
res.status(500, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlockTipHeight(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getBlockHeightTip();
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTxIdsForBlock(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
public getTransactionOutspends(req: Request, res: Response) {
|
||||
res.status(501).send('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routes();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"lib": ["es2019"],
|
||||
"target": "es2015",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"severity": "warn"
|
||||
},
|
||||
"eofline": true,
|
||||
"forin": false,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs",
|
||||
|
||||
1187
backend/yarn.lock
Normal file
1187
backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
51
entrypoint.sh
Normal file
51
entrypoint.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
## Start SQL
|
||||
mysqld_safe&
|
||||
sleep 5
|
||||
## http server:
|
||||
nginx
|
||||
|
||||
## Set up some files:
|
||||
cd /mempool.space/backend
|
||||
rm -f cache.json
|
||||
touch cache.json
|
||||
|
||||
## Build mempool-config.json file ourseleves.
|
||||
## We used to use jq for this but that produced output which caused bugs,
|
||||
## specifically numbers were surrounded by quotes, which breaks things.
|
||||
## Old command was jq -n env > mempool-config.json
|
||||
## This way is more complex, but more compatible with the backend functions.
|
||||
|
||||
## Define a function to allow us to easily get indexes of the = string in from the env output:
|
||||
strindex() {
|
||||
x="${1%%$2*}"
|
||||
[[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
|
||||
}
|
||||
## Regex to check if we have a number or not:
|
||||
NumberRegEx='^[0-9]+$'
|
||||
## Delete the old file, and start a new one:
|
||||
rm -f mempool-config.json
|
||||
echo "{" >> mempool-config.json
|
||||
## For each env we add into the mempool-config.json file in one of two ways.
|
||||
## Either:
|
||||
## "Variable": "Value",
|
||||
## if a string, or
|
||||
## "Variable": Value,
|
||||
## if a integer
|
||||
for e in `env`; do
|
||||
if [[ ${e:`strindex "$e" "="`+1} =~ $NumberRegEx ]] ; then
|
||||
## Integer add:
|
||||
echo "\""${e:0:`strindex "$e" "="`}"\": "${e:`strindex "$e" "="`+1}"," >> mempool-config.json
|
||||
else
|
||||
## String add:
|
||||
echo "\""${e:0:`strindex "$e" "="`}"\": \""${e:`strindex "$e" "="`+1}$"\"," >> mempool-config.json
|
||||
fi
|
||||
done
|
||||
## Take out the trailing , from the last entry.
|
||||
## This means replacing the file with one that is missing the last character
|
||||
echo `sed '$ s/.$//' mempool-config.json` > mempool-config.json
|
||||
## And finally finish off:
|
||||
echo "}" >> mempool-config.json
|
||||
|
||||
## Start mempoolspace:
|
||||
node dist/index.js
|
||||
@@ -1,12 +0,0 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
17
frontend/.gitignore
vendored
17
frontend/.gitignore
vendored
@@ -4,18 +4,10 @@
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
server.run.js
|
||||
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events.json
|
||||
speed-measure-plugin.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
@@ -31,7 +23,6 @@ speed-measure-plugin.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
@@ -46,11 +37,3 @@ testem.log
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
src/resources/assets.json
|
||||
src/resources/assets.minimal.json
|
||||
src/resources/pools.json
|
||||
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
generated-config.js
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[mempool.frontend-src-locale-messages-xlf--master]
|
||||
file_filter = frontend/src/locale/messages.<lang>.xlf
|
||||
source_lang = en-US
|
||||
type = XLIFF
|
||||
@@ -1,37 +0,0 @@
|
||||
FROM node:12-buster-slim AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential rsync
|
||||
RUN npm i
|
||||
RUN npm run build
|
||||
|
||||
RUN mv ./docker/* .
|
||||
|
||||
FROM nginx:1.17.8-alpine
|
||||
|
||||
WORKDIR /patch
|
||||
|
||||
COPY --from=builder /build/entrypoint.sh .
|
||||
COPY --from=builder /build/wait-for .
|
||||
COPY --from=builder /build/dist/mempool /var/www/mempool
|
||||
COPY --from=builder /build/nginx.conf /etc/nginx/
|
||||
COPY --from=builder /build/nginx-mempool.conf /etc/nginx/conf.d/
|
||||
|
||||
RUN chmod +x /patch/entrypoint.sh
|
||||
RUN chmod +x /patch/wait-for
|
||||
|
||||
RUN chown -R 1000:1000 /patch && chmod -R 755 /patch && \
|
||||
chown -R 1000:1000 /var/cache/nginx && \
|
||||
chown -R 1000:1000 /var/log/nginx && \
|
||||
chown -R 1000:1000 /etc/nginx/conf.d
|
||||
RUN touch /var/run/nginx.pid && \
|
||||
chown -R 1000:1000 /var/run/nginx.pid
|
||||
|
||||
USER 1000
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/patch/entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,31 +0,0 @@
|
||||
# mempool-frontend
|
||||
|
||||
## Transifex Project
|
||||
|
||||
The mempool frontend strings are localized into 20+ locales:
|
||||
https://www.transifex.com/mempool/mempool/dashboard/
|
||||
|
||||
## Translators
|
||||
|
||||
* Arabic @baro0k
|
||||
* Czech @pixelmade2
|
||||
* German @Emzy
|
||||
* English (default)
|
||||
* Spanish @maxhodler @bisqes
|
||||
* Persian @techmix
|
||||
* French @Bayernatoor
|
||||
* Korean @kcalvinalvinn
|
||||
* Italian @HodlBits
|
||||
* Georgian @wyd_idk
|
||||
* Hungarian @btcdragonlord
|
||||
* Dutch @m__btc
|
||||
* Japanese @wiz @japananon
|
||||
* Norwegian @T82771355
|
||||
* Portugese @jgcastro1985
|
||||
* Slovenian @thepkbadger
|
||||
* Finnish @bio_bitcoin
|
||||
* Swedish @softsimon_
|
||||
* Turkish @stackmore
|
||||
* Ukrainian @volbil
|
||||
* Vietnamese @bitcoin_vietnam
|
||||
* Chinese @wdljt
|
||||
@@ -4,129 +4,32 @@
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"mempool": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"i18n": {
|
||||
"sourceLocale": {
|
||||
"code":"en-US",
|
||||
"baseHref":"/"
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"translation": "src/locale/messages.ar.xlf",
|
||||
"baseHref": "/ar/"
|
||||
},
|
||||
"cs": {
|
||||
"translation": "src/locale/messages.cs.xlf",
|
||||
"baseHref": "/cs/"
|
||||
},
|
||||
"de": {
|
||||
"translation": "src/locale/messages.de.xlf",
|
||||
"baseHref": "/de/"
|
||||
},
|
||||
"es": {
|
||||
"translation": "src/locale/messages.es.xlf",
|
||||
"baseHref": "/es/"
|
||||
},
|
||||
"fa": {
|
||||
"translation": "src/locale/messages.fa.xlf",
|
||||
"baseHref": "/fa/"
|
||||
},
|
||||
"fr": {
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"baseHref": "/ja/"
|
||||
},
|
||||
"ka": {
|
||||
"translation": "src/locale/messages.ka.xlf",
|
||||
"baseHref": "/ka/"
|
||||
},
|
||||
"ko": {
|
||||
"translation": "src/locale/messages.ko.xlf",
|
||||
"baseHref": "/ko/"
|
||||
},
|
||||
"it": {
|
||||
"translation": "src/locale/messages.it.xlf",
|
||||
"baseHref": "/it/"
|
||||
},
|
||||
"nl": {
|
||||
"translation": "src/locale/messages.nl.xlf",
|
||||
"baseHref": "/nl/"
|
||||
},
|
||||
"nb": {
|
||||
"translation": "src/locale/messages.nb.xlf",
|
||||
"baseHref": "/nb/"
|
||||
},
|
||||
"pt": {
|
||||
"translation": "src/locale/messages.pt.xlf",
|
||||
"baseHref": "/pt/"
|
||||
},
|
||||
"sl": {
|
||||
"translation": "src/locale/messages.sl.xlf",
|
||||
"baseHref": "/sl/"
|
||||
},
|
||||
"sv": {
|
||||
"translation": "src/locale/messages.sv.xlf",
|
||||
"baseHref": "/sv/"
|
||||
},
|
||||
"tr": {
|
||||
"translation": "src/locale/messages.tr.xlf",
|
||||
"baseHref": "/tr/"
|
||||
},
|
||||
"uk": {
|
||||
"translation": "src/locale/messages.uk.xlf",
|
||||
"baseHref": "/uk/"
|
||||
},
|
||||
"fi": {
|
||||
"translation": "src/locale/messages.fi.xlf",
|
||||
"baseHref": "/fi/"
|
||||
},
|
||||
"vi": {
|
||||
"translation": "src/locale/messages.vi.xlf",
|
||||
"baseHref": "/vi/"
|
||||
},
|
||||
"hu": {
|
||||
"translation": "src/locale/messages.hu.xlf",
|
||||
"baseHref": "/hu/"
|
||||
},
|
||||
"zh": {
|
||||
"translation": "src/locale/messages.zh.xlf",
|
||||
"baseHref": "/zh/"
|
||||
}
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"styleext": "scss"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/mempool/browser",
|
||||
"outputPath": "dist/mempool",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/resources",
|
||||
"src/robots.txt"
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
"generated-config.js"
|
||||
]
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -141,20 +44,27 @@
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
"buildOptimizer": true
|
||||
},
|
||||
"electrs": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment-electrs.prod.ts"
|
||||
}
|
||||
]
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -180,93 +90,54 @@
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/resources"
|
||||
],
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json",
|
||||
"tsconfig.server.json"
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"mempool-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "mempool:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "mempool:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"builder": "@angular-devkit/build-angular:server",
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"outputPath": "dist/mempool/server",
|
||||
"main": "server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "media",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"sourceMap": false,
|
||||
"localize": true,
|
||||
"optimization": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-ssr": {
|
||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build",
|
||||
"serverTarget": "mempool:server"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerender": {
|
||||
"builder": "@nguniversal/builders:prerender",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production",
|
||||
"routes": [
|
||||
"/"
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}},
|
||||
}
|
||||
},
|
||||
"defaultProject": "mempool"
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__=${BACKEND_MAINNET_HTTP_HOST:=127.0.0.1}
|
||||
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
|
||||
|
||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__/${__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
||||
|
||||
exec "$@"
|
||||
@@ -1,62 +0,0 @@
|
||||
access_log /var/log/nginx/access_mempool.log;
|
||||
error_log /var/log/nginx/error_mempool.log;
|
||||
|
||||
root /var/www/mempool/browser;
|
||||
|
||||
index index.html;
|
||||
|
||||
# fallback for all URLs i.e. /address/foo /tx/foo /block/000
|
||||
location / {
|
||||
try_files /$lang/$uri /$lang/$uri/ $uri $uri/ /en-US/$uri @index-redirect;
|
||||
}
|
||||
location @index-redirect {
|
||||
add_header vary accept-language;
|
||||
rewrite (.*) /$lang/index.html;
|
||||
}
|
||||
|
||||
# location block using regex are matched in order
|
||||
|
||||
# used to rewrite resources from /<lang>/ to /en-US/
|
||||
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|ka|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/resources/ {
|
||||
rewrite ^/[a-zA-Z-]*/resources/(.*) /en-US/resources/$1;
|
||||
}
|
||||
# used for cookie override
|
||||
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|ka|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/ {
|
||||
try_files $uri $uri/ /$1/index.html =404;
|
||||
}
|
||||
|
||||
# static API docs
|
||||
location = /api {
|
||||
try_files $uri $uri/ /en-US/index.html =404;
|
||||
}
|
||||
location = /api/ {
|
||||
try_files $uri $uri/ /en-US/index.html =404;
|
||||
}
|
||||
|
||||
# mainnet API
|
||||
location /api/v1/donations {
|
||||
proxy_pass https://mempool.space;
|
||||
}
|
||||
location /api/v1/donations/images {
|
||||
proxy_pass https://mempool.space;
|
||||
}
|
||||
location /api/v1/ws {
|
||||
proxy_pass http://__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
location /api/v1 {
|
||||
proxy_pass http://__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/api/v1;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/api/v1/;
|
||||
}
|
||||
|
||||
# mainnet API
|
||||
location /ws {
|
||||
proxy_pass http://__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 100000;
|
||||
|
||||
events {
|
||||
worker_connections 9000;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
|
||||
server_tokens off;
|
||||
server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# reset timed out connections freeing ram
|
||||
reset_timedout_connection on;
|
||||
# maximum time between packets the client can pause when sending nginx any data
|
||||
client_body_timeout 10s;
|
||||
# maximum time the client has to send the entire header to nginx
|
||||
client_header_timeout 10s;
|
||||
# timeout which a single keep-alive client connection will stay open
|
||||
keepalive_timeout 69s;
|
||||
# maximum time between packets nginx is allowed to pause when sending the client data
|
||||
send_timeout 10s;
|
||||
|
||||
# number of requests per connection, does not affect SPDY
|
||||
keepalive_requests 100;
|
||||
|
||||
# enable gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
# text/html is always compressed by gzip module
|
||||
gzip_types application/javascript application/json application/ld+json application/manifest+json application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard;
|
||||
|
||||
# limit request body size
|
||||
client_max_body_size 10m;
|
||||
|
||||
# proxy cache
|
||||
proxy_cache off;
|
||||
proxy_cache_path /var/cache/nginx keys_zone=cache:20m levels=1:2 inactive=600s max_size=500m;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# exempt localhost from rate limit
|
||||
geo $limited_ip {
|
||||
default 1;
|
||||
0.0.0.0 0;
|
||||
}
|
||||
map $limited_ip $limited_ip_key {
|
||||
1 $binary_remote_addr;
|
||||
0 '';
|
||||
}
|
||||
|
||||
# rate limit requests
|
||||
limit_req_zone $limited_ip_key zone=api:5m rate=200r/m;
|
||||
limit_req_zone $limited_ip_key zone=electrs:5m rate=2000r/m;
|
||||
limit_req_status 429;
|
||||
|
||||
# rate limit connections
|
||||
limit_conn_zone $limited_ip_key zone=websocket:10m;
|
||||
limit_conn_status 429;
|
||||
|
||||
map $http_accept_language $header_lang {
|
||||
default en-US;
|
||||
~*^en-US en-US;
|
||||
~*^en en-US;
|
||||
~*^ar ar;
|
||||
~*^cs cs;
|
||||
~*^de de;
|
||||
~*^es es;
|
||||
~*^fa fa;
|
||||
~*^fr fr;
|
||||
~*^ja ja;
|
||||
~*^ka ka;
|
||||
~*^hu hu;
|
||||
~*^nl nl;
|
||||
~*^nn nn;
|
||||
~*^pt pt;
|
||||
~*^sl sl;
|
||||
~*^sv sv;
|
||||
~*^tr tr;
|
||||
~*^uk uk;
|
||||
~*^vi vi;
|
||||
~*^zh zh;
|
||||
}
|
||||
|
||||
map $cookie_lang $lang {
|
||||
default $header_lang;
|
||||
~*^en-US en-US;
|
||||
~*^en en-US;
|
||||
~*^ar ar;
|
||||
~*^cs cs;
|
||||
~*^de de;
|
||||
~*^es es;
|
||||
~*^fa fa;
|
||||
~*^fr fr;
|
||||
~*^ja ja;
|
||||
~*^ka ka;
|
||||
~*^hu hu;
|
||||
~*^nl nl;
|
||||
~*^nn nn;
|
||||
~*^pt pt;
|
||||
~*^sl sl;
|
||||
~*^sv sv;
|
||||
~*^tr tr;
|
||||
~*^uk uk;
|
||||
~*^vi vi;
|
||||
~*^zh zh;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 0.0.0.0:8080;
|
||||
include /etc/nginx/conf.d/nginx-mempool.conf;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
TIMEOUT=15
|
||||
QUIET=0
|
||||
|
||||
echoerr() {
|
||||
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
exitcode="$1"
|
||||
cat << USAGE >&2
|
||||
Usage:
|
||||
$cmdname host:port [-t timeout] [-- command args]
|
||||
-q | --quiet Do not output any status messages
|
||||
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
|
||||
-- COMMAND ARGS Execute command with args after the test finishes
|
||||
USAGE
|
||||
exit "$exitcode"
|
||||
}
|
||||
|
||||
wait_for() {
|
||||
if ! command -v nc >/dev/null; then
|
||||
echoerr 'nc command is missing!'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in `seq $TIMEOUT` ; do
|
||||
nc -z "$HOST" "$PORT" > /dev/null 2>&1
|
||||
|
||||
result=$?
|
||||
if [ $result -eq 0 ] ; then
|
||||
if [ $# -gt 0 ] ; then
|
||||
exec "$@"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "Operation timed out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]
|
||||
do
|
||||
case "$1" in
|
||||
*:* )
|
||||
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
|
||||
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
|
||||
shift 1
|
||||
;;
|
||||
-q | --quiet)
|
||||
QUIET=1
|
||||
shift 1
|
||||
;;
|
||||
-t)
|
||||
TIMEOUT="$2"
|
||||
if [ "$TIMEOUT" = "" ]; then break; fi
|
||||
shift 2
|
||||
;;
|
||||
--timeout=*)
|
||||
TIMEOUT="${1#*=}"
|
||||
shift 1
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
--help)
|
||||
usage 0
|
||||
;;
|
||||
*)
|
||||
echoerr "Unknown argument: $1"
|
||||
usage 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$HOST" = "" -o "$PORT" = "" ]; then
|
||||
echoerr "Error: you need to provide a host and port to test."
|
||||
usage 2
|
||||
fi
|
||||
|
||||
wait_for "$@"
|
||||
@@ -1,12 +1,8 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
@@ -25,7 +21,7 @@ exports.config = {
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
@@ -10,14 +9,6 @@ describe('workspace-project App', () => {
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to mempool!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
expect(page.getParagraphText()).toEqual('Welcome to app!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@ import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
getParagraphText() {
|
||||
return element(by.css('app-root h1')).getText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.
|
||||
@@ -1,36 +0,0 @@
|
||||
var fs = require('fs');
|
||||
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
|
||||
|
||||
let settings = [];
|
||||
let configContent = {};
|
||||
|
||||
try {
|
||||
const rawConfig = fs.readFileSync(CONFIG_FILE_NAME);
|
||||
configContent = JSON.parse(rawConfig);
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
for (setting in configContent) {
|
||||
settings.push({
|
||||
key: setting,
|
||||
value: configContent[setting]
|
||||
});
|
||||
}
|
||||
|
||||
const code = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
|
||||
}(global || this));`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
|
||||
console.log('Config file generated');
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"TESTNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"BISQ_ENABLED": false,
|
||||
"BISQ_SEPARATE_BACKEND": false,
|
||||
"ITEMS_PER_PAGE": 10,
|
||||
"KEEP_BLOCKS_AMOUNT": 8,
|
||||
"SPONSORS_ENABLED": false,
|
||||
"NGINX_PROTOCOL": "http",
|
||||
"NGINX_HOSTNAME": "127.0.0.1",
|
||||
"NGINX_PORT": "80"
|
||||
}
|
||||
33448
frontend/package-lock.json
generated
33448
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,96 +1,50 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.0.0",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "MIT",
|
||||
"homepage": "https://mempool.space",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mempool/mempool"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/mempool/mempool/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"bitcoin",
|
||||
"mempool",
|
||||
"blockchain",
|
||||
"explorer",
|
||||
"liquid"
|
||||
],
|
||||
"main": "index.ts",
|
||||
"version": "1.0.0",
|
||||
"description": "Bitcoin Mempool Visualizer",
|
||||
"scripts": {
|
||||
"ng": "./node_modules/@angular/cli/bin/ng",
|
||||
"tsc": "./node_modules/typescript/bin/tsc",
|
||||
"i18n-extract-from-source": "./node_modules/@angular/cli/bin/ng xi18n --ivy --out-file ./src/locale/messages.xlf",
|
||||
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
|
||||
"serve": "ng serve --proxy-config proxy.conf.json",
|
||||
"start": "npm run generate-config && npm run sync-assets-dev && ng serve --proxy-config proxy.conf.json",
|
||||
"build": "npm run generate-config && ng build --prod --localize && npm run sync-assets",
|
||||
"sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
|
||||
"sync-assets-dev": "node sync-assets.js dev",
|
||||
"generate-config": "node generate-config.js",
|
||||
"ng": "ng",
|
||||
"start": "ng serve --aot --proxy-config proxy.conf.json",
|
||||
"build": "ng build --prod",
|
||||
"build-electrs": "ng build --prod --configuration=electrs",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "node server.run.js",
|
||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||
"prerender": "ng run mempool:prerender"
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"author": {
|
||||
"name": "Simon Lindh",
|
||||
"url": "https://github.com/mempool-space/mempool.space"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "~10.2.3",
|
||||
"@angular/common": "~10.2.3",
|
||||
"@angular/compiler": "~10.2.3",
|
||||
"@angular/core": "~10.2.3",
|
||||
"@angular/forms": "~10.2.3",
|
||||
"@angular/localize": "^10.2.3",
|
||||
"@angular/platform-browser": "~10.2.3",
|
||||
"@angular/platform-browser-dynamic": "~10.2.3",
|
||||
"@angular/platform-server": "~10.2.2",
|
||||
"@angular/router": "~10.2.3",
|
||||
"@fortawesome/angular-fontawesome": "^0.7.0",
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.30",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||
"@mempool/chartist": "^0.11.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
|
||||
"@nguniversal/express-engine": "10.1.0",
|
||||
"@types/qrcode": "^1.3.4",
|
||||
"bootstrap": "4.5.0",
|
||||
"clipboard": "^2.0.4",
|
||||
"domino": "^2.1.6",
|
||||
"express": "^4.15.2",
|
||||
"ngx-bootrap-multiselect": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^9.0.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"rxjs": "^6.6.3",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.3"
|
||||
"@angular/animations": "^8.2.11",
|
||||
"@angular/common": "^8.2.11",
|
||||
"@angular/compiler": "^8.2.11",
|
||||
"@angular/core": "^8.2.11",
|
||||
"@angular/forms": "^8.2.11",
|
||||
"@angular/platform-browser": "^8.2.11",
|
||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||
"@angular/router": "^8.2.11",
|
||||
"@ng-bootstrap/ng-bootstrap": "^5.1.1",
|
||||
"angularx-qrcode": "^1.7.0-beta.5",
|
||||
"bootstrap": "^4.3.1",
|
||||
"chartist": "^0.11.2",
|
||||
"core-js": "^3.4.1",
|
||||
"ng-chartist": "^2.0.0-beta.1",
|
||||
"rxjs": "^6.5.3",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.1002.0",
|
||||
"@angular/cli": "~10.2.0",
|
||||
"@angular/compiler-cli": "~10.2.2",
|
||||
"@angular/language-service": "~10.2.2",
|
||||
"@nguniversal/builders": "^10.1.0",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/jasmine": "~3.3.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~5.0.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~3.3.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"@angular-devkit/build-angular": "~0.800.0",
|
||||
"@angular/cli": "~8.3.12",
|
||||
"@angular/compiler-cli": "^8.2.11",
|
||||
"@angular/language-service": "^8.2.11",
|
||||
"@types/chartist": "^0.9.46",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~5.1.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.0.5"
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,11 @@
|
||||
{
|
||||
"/api/v1": {
|
||||
"/api": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false
|
||||
},
|
||||
"/api/v1/ws": {
|
||||
"/ws": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/api/": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/api/": "/api/v1/"
|
||||
}
|
||||
},
|
||||
"/testnet/api/v1": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api/v1": "/api/v1"
|
||||
}
|
||||
},
|
||||
"/testnet/api/v1/ws": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/testnet/api/": {
|
||||
"target": "http://localhost:50001/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/testnet/api": ""
|
||||
}
|
||||
},
|
||||
"/liquid/api/v1/ws": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api": "/api/v1/ws"
|
||||
}
|
||||
},
|
||||
"/liquid/api/": {
|
||||
"target": "http://localhost:50001/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/liquid/api/": ""
|
||||
}
|
||||
},
|
||||
"/bisq/api/": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
}
|
||||
},
|
||||
"/bisq/api/v1/ws": {
|
||||
"target": "http://localhost:8999/",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"pathRewrite": {
|
||||
"^/bisq/api": "/api/v1/ws"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'zone.js/dist/zone-node';
|
||||
import './generated-config';
|
||||
|
||||
import * as domino from 'domino';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const {readFileSync, existsSync} = require('fs');
|
||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = () => {
|
||||
return {
|
||||
matches: true
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the list of supported and actually active locales
|
||||
*/
|
||||
function getActiveLocales() {
|
||||
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
|
||||
|
||||
const supportedLocales = [
|
||||
angularConfig.projects.mempool.i18n.sourceLocale,
|
||||
...Object.keys(angularConfig.projects.mempool.i18n.locales),
|
||||
];
|
||||
|
||||
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
|
||||
}
|
||||
|
||||
function app() {
|
||||
const server = express();
|
||||
|
||||
// proxy API to nginx
|
||||
server.get('/api/**', createProxyMiddleware({
|
||||
// @ts-ignore
|
||||
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
|
||||
changeOrigin: true,
|
||||
}));
|
||||
|
||||
// map / and /en to en-US
|
||||
const defaultLocale = 'en-US';
|
||||
console.log(`serving default locale: ${defaultLocale}`);
|
||||
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
|
||||
server.use('/', appServerModule.app(defaultLocale));
|
||||
server.use('/en', appServerModule.app(defaultLocale));
|
||||
|
||||
// map each locale to its localized main.js
|
||||
getActiveLocales().forEach(locale => {
|
||||
console.log('serving locale:', locale);
|
||||
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
|
||||
|
||||
// map everything to itself
|
||||
server.use(`/${locale}`, appServerModule.app(locale));
|
||||
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
app().listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,146 +0,0 @@
|
||||
import 'zone.js/dist/zone-node';
|
||||
import './generated-config';
|
||||
|
||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as domino from 'domino';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
import { join } from 'path';
|
||||
import { AppServerModule } from './src/main.server';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = () => {
|
||||
return {
|
||||
matches: true
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app(locale: string): express.Express {
|
||||
const server = express();
|
||||
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
|
||||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
||||
|
||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||
server.engine('html', ngExpressEngine({
|
||||
bootstrap: AppServerModule,
|
||||
}));
|
||||
|
||||
server.set('view engine', 'html');
|
||||
server.set('views', distFolder);
|
||||
|
||||
// only handle URLs that actually exist
|
||||
//server.get(locale, getLocalizedSSR(indexHtml));
|
||||
server.get('/', getLocalizedSSR(indexHtml));
|
||||
server.get('/tx/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/address/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/blocks', getLocalizedSSR(indexHtml));
|
||||
server.get('/graphs', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/api', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/status', getLocalizedSSR(indexHtml));
|
||||
server.get('/liquid/about', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/api', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/status', getLocalizedSSR(indexHtml));
|
||||
server.get('/testnet/about', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/about', getLocalizedSSR(indexHtml));
|
||||
server.get('/bisq/api', getLocalizedSSR(indexHtml));
|
||||
server.get('/about', getLocalizedSSR(indexHtml));
|
||||
server.get('/api', getLocalizedSSR(indexHtml));
|
||||
server.get('/tv', getLocalizedSSR(indexHtml));
|
||||
server.get('/status', getLocalizedSSR(indexHtml));
|
||||
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
|
||||
|
||||
// fallback to static file handler so we send HTTP 404 to nginx
|
||||
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function getLocalizedSSR(indexHtml) {
|
||||
return (req, res) => {
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// only used for development mode
|
||||
function run(): void {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
const server = app('en-US');
|
||||
server.listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Webpack will replace 'require' with '__webpack_require__'
|
||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||
declare const __non_webpack_require__: NodeRequire;
|
||||
const mainModule = __non_webpack_require__.main;
|
||||
const moduleFilename = mainModule && mainModule.filename || '';
|
||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||
run();
|
||||
}
|
||||
|
||||
export * from './src/main.server';
|
||||
42
frontend/src/app/about/about.component.html
Normal file
42
frontend/src/app/about/about.component.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<div class="text-center">
|
||||
<img src="./assets/mempool-tube.png" width="63" height="63" />
|
||||
<br /><br />
|
||||
|
||||
<h2>About</h2>
|
||||
|
||||
<p>Mempool.Space is a realtime Bitcoin blockchain visualizer and statistics website focused on SegWit.</p>
|
||||
<p>Created by <a href="http://t.me/softcrypto">@softcrypto</a> (Telegram). <a href="https://twitter.com/softcrypt0">@softcrypt0</a> (Twitter).
|
||||
<br />Designed by <a href="https://emeraldo.io">emeraldo.io</a>.
|
||||
<br />Hosted by <a href="https://twitter.com/wiz">@wiz</a></p>
|
||||
|
||||
|
||||
<h2>Fee API</h2>
|
||||
|
||||
<div class="col-4 mx-auto">
|
||||
<input class="form-control" type="text" value="https://mempool.space/api/v1/fees/recommended" readonly>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<h1>Donate</h1>
|
||||
<h3>Segwit native</h3>
|
||||
<img src="./assets/btc-qr-code-segwit.png" width="200" height="200" />
|
||||
<br />
|
||||
bc1qqrmgr60uetlmrpylhtllawyha9z5gw6hwdmk2t
|
||||
|
||||
<br /><br />
|
||||
<h3>Segwit compatibility</h3>
|
||||
<img src="./assets/btc-qr-code.png" width="200" height="200" />
|
||||
<br />
|
||||
3Ccig4G4u8hbExnxBJHeE5ZmxxWxvEQ65f
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<h3>PayNym</h3>
|
||||
<img src="./assets/paynym-code.png" width="200" height="200" />
|
||||
<br />
|
||||
<p style="word-wrap: break-word; overflow-wrap: break-word;max-width: 300px; text-align: center; margin: auto;">
|
||||
PM8TJZWDn1XbYmVVMR3RP9Kt1BW69VCSLTC12UB8iWUiKcEBJsxB4UUKBMJxc3LVaxtU5d524sLFrTy9kFuyPQ73QkEagGcMfCE6M38E5C67EF8KAqvS
|
||||
</p>
|
||||
</div>
|
||||
19
frontend/src/app/about/about.component.ts
Normal file
19
frontend/src/app/about/about.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ApiService } from '../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
templateUrl: './about.component.html',
|
||||
styleUrls: ['./about.component.scss']
|
||||
})
|
||||
export class AboutComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.apiService.webSocketWant([]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsComponent } from './assets/assets.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { BlockchainComponent } from './blockchain/blockchain.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { TelevisionComponent } from './television/television.component';
|
||||
import { MasterPageComponent } from './master-page/master-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -24,207 +13,44 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
component: BlockchainComponent
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
path: 'tx/:id',
|
||||
children: [],
|
||||
component: BlockchainComponent
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
children: [],
|
||||
component: AboutComponent
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
component: AboutComponent,
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
component: ApiDocsComponent,
|
||||
},
|
||||
{
|
||||
path: 'terms-of-service',
|
||||
component: TermsOfServiceComponent
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent
|
||||
path: 'explorer',
|
||||
loadChildren: './explorer/explorer.module#ExplorerModule',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'liquid',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
component: ApiDocsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
component: TelevisionComponent
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
component: TransactionComponent
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
component: BlockComponent
|
||||
},
|
||||
{
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'blocks',
|
||||
component: LatestBlocksComponent,
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
children: [],
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
component: ApiDocsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
component: TelevisionComponent
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'bisq',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
component: TelevisionComponent,
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabled'
|
||||
})],
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
||||
1
frontend/src/app/app.component.html
Normal file
1
frontend/src/app/app.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
||||
10
frontend/src/app/app.component.ts
Normal file
10
frontend/src/app/app.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
export const mempoolFeeColors = [
|
||||
'557d00',
|
||||
'5d7d01',
|
||||
'637d02',
|
||||
'6d7d04',
|
||||
'757d05',
|
||||
'7d7d06',
|
||||
'867d08',
|
||||
'8c7d09',
|
||||
'957d0b',
|
||||
'9b7d0c',
|
||||
'a67d0e',
|
||||
'aa7d0f',
|
||||
'b27d10',
|
||||
'bb7d11',
|
||||
'bf7d12',
|
||||
'bf7815',
|
||||
'bf7319',
|
||||
'be6c1e',
|
||||
'be6820',
|
||||
'bd6125',
|
||||
'bd5c28',
|
||||
'bc552d',
|
||||
'bc4f30',
|
||||
'bc4a34',
|
||||
'bb4339',
|
||||
'bb3d3c',
|
||||
'bb373f',
|
||||
'ba3243',
|
||||
'b92b48',
|
||||
'b9254b',
|
||||
];
|
||||
|
||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const languages: Language[] = [
|
||||
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||
// { code: 'bg', name: 'Български' }, // Bulgarian
|
||||
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||
// { code: 'ca', name: 'Català' }, // Catalan
|
||||
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||
// { code: 'da', name: 'Dansk' }, // Danish
|
||||
{ code: 'de', name: 'Deutsch' }, // German
|
||||
// { code: 'et', name: 'Eesti' }, // Estonian
|
||||
// { code: 'el', name: 'Ελληνικά' }, // Greek
|
||||
{ code: 'en', name: 'English' }, // English
|
||||
{ code: 'es', name: 'Español' }, // Spanish
|
||||
// { code: 'eo', name: 'Esperanto' }, // Esperanto
|
||||
// { code: 'eu', name: 'Euskara' }, // Basque
|
||||
{ code: 'fa', name: 'فارسی' }, // Persian
|
||||
{ code: 'fr', name: 'Français' }, // French
|
||||
// { code: 'gl', name: 'Galego' }, // Galician
|
||||
{ code: 'ko', name: '한국어' }, // Korean
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'it', name: 'Italiano' }, // Italian
|
||||
// { code: 'he', name: 'עברית' }, // Hebrew
|
||||
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||
// { code: 'lv', name: 'Latviešu' }, // Latvian
|
||||
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
||||
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
||||
// { code: 'mk', name: 'Македонски' }, // Macedonian
|
||||
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
||||
{ code: 'nl', name: 'Nederlands' }, // Dutch
|
||||
{ code: 'ja', name: '日本語' }, // Japanese
|
||||
{ code: 'nb', name: 'Norsk' }, // Norwegian Bokmål
|
||||
// { code: 'nn', name: 'Norsk Nynorsk' }, // Norwegian Nynorsk
|
||||
// { code: 'pl', name: 'Polski' }, // Polish
|
||||
{ code: 'pt', name: 'Português' }, // Portuguese
|
||||
// { code: 'pt-BR', name: 'Português (Brazil)' }, // Portuguese (Brazil)
|
||||
// { code: 'ro', name: 'Română' }, // Romanian
|
||||
// { code: 'ru', name: 'Русский' }, // Russian
|
||||
// { code: 'sk', name: 'Slovenčina' }, // Slovak
|
||||
{ code: 'sl', name: 'Slovenščina' }, // Slovenian
|
||||
// { code: 'sr', name: 'Српски / srpski' }, // Serbian
|
||||
// { code: 'sh', name: 'Srpskohrvatski / српскохрватски' },// Serbo-Croatian
|
||||
{ code: 'fi', name: 'Suomi' }, // Finnish
|
||||
{ code: 'sv', name: 'Svenska' }, // Swedish
|
||||
// { code: 'th', name: 'ไทย' }, // Thai
|
||||
{ code: 'tr', name: 'Türkçe' }, // Turkish
|
||||
{ code: 'uk', name: 'Українська' }, // Ukrainian
|
||||
{ code: 'vi', name: 'Tiếng Việt' }, // Vietnamese
|
||||
{ code: 'zh', name: '中文' }, // Chinese
|
||||
];
|
||||
@@ -1,129 +1,57 @@
|
||||
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { BlockchainComponent } from './blockchain/blockchain.component';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
|
||||
import { AmountComponent } from './components/amount/amount.component';
|
||||
import { StateService } from './services/state.service';
|
||||
import { BlockComponent } from './components/block/block.component';
|
||||
import { AddressComponent } from './components/address/address.component';
|
||||
import { SearchFormComponent } from './components/search-form/search-form.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
|
||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
|
||||
import { MasterPageComponent } from './components/master-page/master-page.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { TelevisionComponent } from './components/television/television.component';
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
import { ChartistComponent } from './components/statistics/chartist.component';
|
||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
|
||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
|
||||
import { TimespanComponent } from './components/timespan/timespan.component';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsComponent } from './assets/assets.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { MinerComponent } from './components/miner/miner.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faExchangeAlt, faInfoCircle,
|
||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { MemPoolService } from './services/mem-pool.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { AboutComponent } from './about/about.component';
|
||||
import { TxBubbleComponent } from './tx-bubble/tx-bubble.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component';
|
||||
import { TelevisionComponent } from './television/television.component';
|
||||
import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component';
|
||||
import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component';
|
||||
import { ApiService } from './services/api.service';
|
||||
import { MasterPageComponent } from './master-page/master-page.component';
|
||||
import { FeeDistributionGraphComponent } from './fee-distribution-graph/fee-distribution-graph.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
AboutComponent,
|
||||
MasterPageComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainComponent,
|
||||
StartComponent,
|
||||
BlockchainBlocksComponent,
|
||||
StatisticsComponent,
|
||||
TransactionComponent,
|
||||
BlockComponent,
|
||||
TransactionsListComponent,
|
||||
AddressComponent,
|
||||
AmountComponent,
|
||||
LatestBlocksComponent,
|
||||
SearchFormComponent,
|
||||
TimespanComponent,
|
||||
AddressLabelsComponent,
|
||||
MempoolBlocksComponent,
|
||||
ChartistComponent,
|
||||
FooterComponent,
|
||||
MempoolBlockComponent,
|
||||
StatisticsComponent,
|
||||
AboutComponent,
|
||||
TxBubbleComponent,
|
||||
BlockModalComponent,
|
||||
ProjectedBlockModalComponent,
|
||||
TelevisionComponent,
|
||||
BlockchainBlocksComponent,
|
||||
BlockchainProjectedBlocksComponent,
|
||||
MasterPageComponent,
|
||||
FeeDistributionGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
AssetComponent,
|
||||
AssetsComponent,
|
||||
MinerComponent,
|
||||
StatusViewComponent,
|
||||
FeesBoxComponent,
|
||||
DashboardComponent,
|
||||
ApiDocsComponent,
|
||||
TermsOfServiceComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
BrowserTransferStateModule,
|
||||
AppRoutingModule,
|
||||
ReactiveFormsModule,
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
InfiniteScrollModule,
|
||||
NgbTypeaheadModule,
|
||||
FontAwesomeModule,
|
||||
AppRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
ElectrsApiService,
|
||||
StateService,
|
||||
WebsocketService,
|
||||
AudioService,
|
||||
SeoService,
|
||||
StorageService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
ApiService,
|
||||
MemPoolService,
|
||||
],
|
||||
entryComponents: [
|
||||
BlockModalComponent,
|
||||
ProjectedBlockModalComponent,
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(library: FaIconLibrary) {
|
||||
library.addIcons(faInfoCircle);
|
||||
library.addIcons(faChartArea);
|
||||
library.addIcons(faTv);
|
||||
library.addIcons(faTachometerAlt);
|
||||
library.addIcons(faCubes);
|
||||
library.addIcons(faCogs);
|
||||
library.addIcons(faThList);
|
||||
library.addIcons(faList);
|
||||
library.addIcons(faTachometerAlt);
|
||||
library.addIcons(faDatabase);
|
||||
library.addIcons(faSearch);
|
||||
library.addIcons(faLink);
|
||||
library.addIcons(faBolt);
|
||||
library.addIcons(faTint);
|
||||
library.addIcons(faAngleDown);
|
||||
library.addIcons(faAngleUp);
|
||||
library.addIcons(faExchangeAlt);
|
||||
}
|
||||
}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppModule,
|
||||
ServerModule,
|
||||
ServerTransferStateModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
@@ -1,75 +0,0 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="Registered assets page header">Registered assets</h1>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<form [formGroup]="searchForm" class="form-inline">
|
||||
<div class="input-group m-2">
|
||||
<input style="width: 250px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
|
||||
<div class="input-group-append">
|
||||
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th class="td-name" i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th i18n="Asset ID header">Asset ID</th>
|
||||
<th class="d-none d-lg-block" i18n="Asset issuance transaction header">Issuance TX</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
||||
<td class="td-name">{{ asset.name }}</td>
|
||||
<td>{{ asset.ticker }}</td>
|
||||
<td class="d-none d-md-block"><a *ngIf="asset.entity" target="_blank" href="{{ 'http://' + asset.entity.domain }}">{{ asset.entity.domain }}</a></td>
|
||||
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
||||
<td class="d-none d-lg-block"><ng-template [ngIf]="asset.issuance_txin"><a [routerLink]="['/tx/' | relativeUrl, asset.issuance_txin.txid]">{{ asset.issuance_txin.txid | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.issuance_txin.txid"></app-clipboard></ng-template></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #isLoading>
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th i18n="Asset ID header">Asset ID</th>
|
||||
<th i18n="Asset issuance transaction header">Issuance TX</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let dummy of [0,0,0]">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td class="d-none d-lg-block"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,6 +0,0 @@
|
||||
.td-name {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AssetsComponent } from './assets.component';
|
||||
|
||||
describe('AssetsComponent', () => {
|
||||
let component: AssetsComponent;
|
||||
let fixture: ComponentFixture<AssetsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AssetsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AssetsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AssetsService } from '../services/assets.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { merge, combineLatest, Observable } from 'rxjs';
|
||||
import { AssetExtended } from '../interfaces/electrs.interface';
|
||||
import { SeoService } from '../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assets',
|
||||
templateUrl: './assets.component.html',
|
||||
styleUrls: ['./assets.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AssetsComponent implements OnInit {
|
||||
nativeAssetId = environment.nativeAssetId;
|
||||
assets: AssetExtended[];
|
||||
assetsCache: AssetExtended[];
|
||||
searchForm: FormGroup;
|
||||
assets$: Observable<AssetExtended[]>;
|
||||
|
||||
error: any;
|
||||
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (250 + 200);
|
||||
fiveItemsPxSize = 250;
|
||||
|
||||
constructor(
|
||||
private assetsService: AssetsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: [{ value: '', disabled: true }, Validators.required]
|
||||
});
|
||||
|
||||
this.assets$ = combineLatest([
|
||||
this.assetsService.getAssetsJson$,
|
||||
this.route.queryParams
|
||||
])
|
||||
.pipe(
|
||||
take(1),
|
||||
mergeMap(([assets, qp]) => {
|
||||
this.assets = Object.values(assets);
|
||||
// @ts-ignore
|
||||
this.assets.push({
|
||||
name: 'Liquid Bitcoin',
|
||||
ticker: 'L-BTC',
|
||||
asset_id: this.nativeAssetId,
|
||||
});
|
||||
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
this.assetsCache = this.assets;
|
||||
this.searchForm.get('searchText').enable();
|
||||
|
||||
if (qp.search) {
|
||||
this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false });
|
||||
}
|
||||
|
||||
return merge(
|
||||
this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap((text) => {
|
||||
this.page = 1;
|
||||
this.searchTextChanged(text);
|
||||
})
|
||||
),
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
filter((queryParams) => {
|
||||
const newPage = parseInt(queryParams.page, 10);
|
||||
if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
map((queryParams) => {
|
||||
if (queryParams.page) {
|
||||
const newPage = parseInt(queryParams.page, 10);
|
||||
this.page = newPage;
|
||||
} else {
|
||||
this.page = 1;
|
||||
}
|
||||
if (this.searchForm.get('searchText').value !== (queryParams.search || '')) {
|
||||
this.searchTextChanged(queryParams.search);
|
||||
}
|
||||
if (queryParams.search) {
|
||||
this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false });
|
||||
return queryParams.search;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
),
|
||||
);
|
||||
}),
|
||||
map((searchText) => {
|
||||
const start = (this.page - 1) * this.itemsPerPage;
|
||||
if (searchText.length ) {
|
||||
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
||||
|| asset.ticker.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
||||
this.assets = filteredAssets;
|
||||
return filteredAssets.slice(start, this.itemsPerPage + start);
|
||||
} else {
|
||||
this.assets = this.assetsCache;
|
||||
return this.assets.slice(start, this.itemsPerPage + start);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
const queryParams = { page: page, search: this.searchForm.get('searchText').value };
|
||||
if (queryParams.search === '') {
|
||||
queryParams.search = null;
|
||||
}
|
||||
if (queryParams.page === 1) {
|
||||
queryParams.page = null;
|
||||
}
|
||||
this.page = -1;
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
searchTextChanged(text: string) {
|
||||
const queryParams = { search: text, page: 1 };
|
||||
if (queryParams.search === '') {
|
||||
queryParams.search = null;
|
||||
}
|
||||
if (queryParams.page === 1) {
|
||||
queryParams.page = null;
|
||||
}
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
trackByAsset(index: number, asset: any) {
|
||||
return asset.asset_id;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;" i18n="shared.address">Address</h1>
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAddress && !error">
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received">Total received</td>
|
||||
<td>{{ totalReceived / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Total sent</td>
|
||||
<td>{{ totalSent / 100 | number: '1.2-2' }} BSQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ (<app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>
|
||||
<ng-container *ngTemplateOutlet="transactions.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: transactions.length}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="transactions">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAddress && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading address data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,23 +0,0 @@
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { switchMap, filter, catchError } from 'rxjs/operators';
|
||||
import { ParamMap, ActivatedRoute } from '@angular/router';
|
||||
import { Subscription, of } from 'rxjs';
|
||||
import { BisqTransaction } from '../bisq.interfaces';
|
||||
import { BisqApiService } from '../bisq-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-address',
|
||||
templateUrl: './bisq-address.component.html',
|
||||
styleUrls: ['./bisq-address.component.scss']
|
||||
})
|
||||
export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
transactions: BisqTransaction[];
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
|
||||
totalReceived = 0;
|
||||
totalSent = 0;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private bisqApiService: BisqApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAddress = true;
|
||||
this.transactions = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
|
||||
return this.bisqApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
console.log(err);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}),
|
||||
filter((transactions) => transactions !== null)
|
||||
)
|
||||
.subscribe((transactions: BisqTransaction[]) => {
|
||||
this.transactions = transactions;
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.isLoadingAddress = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateChainStats() {
|
||||
const shortenedAddress = this.addressString.substr(1);
|
||||
|
||||
this.totalSent = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.inputs
|
||||
.filter((input) => input.address === shortenedAddress)
|
||||
.reduce((a, input) => a + input.bsqAmount, 0), 0);
|
||||
|
||||
this.totalReceived = this.transactions.reduce((acc, tx) =>
|
||||
acc + tx.outputs
|
||||
.filter((output) => output.address === shortenedAddress)
|
||||
.reduce((a, output) => a + output.bsqAmount, 0), 0);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mainSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces';
|
||||
|
||||
const API_BASE_URL = '/bisq/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BisqApiService {
|
||||
apiBaseUrl: string;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
) { }
|
||||
|
||||
getStats$(): Observable<BisqStats> {
|
||||
return this.httpClient.get<BisqStats>(API_BASE_URL + '/stats');
|
||||
}
|
||||
|
||||
getTransaction$(txId: string): Observable<BisqTransaction> {
|
||||
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/tx/' + txId);
|
||||
}
|
||||
|
||||
listTransactions$(start: number, length: number, types: string[]): Observable<HttpResponse<BisqTransaction[]>> {
|
||||
let params = new HttpParams();
|
||||
types.forEach((t: string) => {
|
||||
params = params.append('types[]', t);
|
||||
});
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/txs/${start}/${length}`, { params, observe: 'response' });
|
||||
}
|
||||
|
||||
getBlock$(hash: string): Observable<BisqBlock> {
|
||||
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/block/' + hash);
|
||||
}
|
||||
|
||||
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
|
||||
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' });
|
||||
}
|
||||
|
||||
getAddress$(address: string): Observable<BisqTransaction[]> {
|
||||
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
<div class="container-xl">
|
||||
|
||||
<div class="title-block">
|
||||
<h1><ng-template [ngIf]="blockHeight" i18n="block.block">Block <a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoading && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
||||
<td>
|
||||
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>
|
||||
<ng-container *ngTemplateOutlet="block.txs.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.txs.length| number}"></ng-container>
|
||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||
</h2>
|
||||
|
||||
<ng-template ngFor let-tx [ngForOf]="block.txs">
|
||||
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
|
||||
|
||||
<br>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoading && !error">
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="text-center">
|
||||
Error loading block
|
||||
<br>
|
||||
<i>{{ error.status }}: {{ error.statusText }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user