Compare commits

...

103 Commits

Author SHA1 Message Date
wiz
ed485fa16a Release v2.4.1 2022-07-25 21:01:04 +02:00
wiz
507c8b18f4 Merge pull request #2153 from knorrium/knorrium/241_cherry_pick2
Fix block predition graph x axis labels
2022-07-23 16:14:09 +02:00
nymkappa
185223bffd Fix block predition graph x axis labels 2022-07-22 21:36:56 -07:00
wiz
dd4e120ab0 Merge pull request #2136 from knorrium/v241_patch
[Indexer] Set log level accordingly - Remove indexing ETAs
2022-07-19 09:12:39 -05:00
nymkappa
ae0789a3fa [Indexer] Set log level accordingly - Remove indexing ETAs 2022-07-18 20:08:15 -07:00
softsimon
ede5508397 Remove random scss calculation 2022-07-18 17:50:27 -05:00
wiz
a99b52a735 Merge pull request #2057 from mempool/simon/transifex-pull-0710
Pull from transifex
2022-07-10 13:57:58 +02:00
wiz
e58b71fd4f Merge pull request #2052 from mempool/nymkappa/bugfix/blocks-list-pagination-error
Fix pagination issue in blocks list
2022-07-10 13:56:38 +02:00
wiz
59d10fd3c6 Merge pull request #2051 from mempool/nymkappa/bugfix/diff-adj-table-raw-db-data
Fix diff adj table using raw db value
2022-07-10 13:14:56 +02:00
wiz
30e8b134bc Merge branch 'master' into nymkappa/bugfix/diff-adj-table-raw-db-data 2022-07-10 12:56:30 +02:00
nymkappa
cdf0fe0335 Fix first diff adjustmeent if INDEXING_BLOCK_AMOUNT is not -1 2022-07-10 12:39:22 +02:00
nymkappa
f4667c0892 Use raw diff adjustment for diff adj table widget 2022-07-10 12:39:21 +02:00
wiz
ac257b4165 Merge branch 'master' into nymkappa/bugfix/blocks-list-pagination-error 2022-07-10 12:37:51 +02:00
wiz
75ab2bc920 Merge pull request #2054 from mempool/nymkappa/bugfix/dont-insert-price-no-usd
Ignore prices without USD exchange rate
2022-07-10 12:37:43 +02:00
wiz
307c30e33b Merge pull request #2056 from mempool/nymkappa/feature/config-automatic-block-reindex
Disable automatic block re-indexing by default
2022-07-10 12:36:43 +02:00
softsimon
c7835b1326 Pull from transifex 2022-07-10 12:29:19 +02:00
wiz
7e389c8863 Merge branch 'master' into nymkappa/bugfix/dont-insert-price-no-usd 2022-07-10 12:17:08 +02:00
wiz
d697c0c45e Merge branch 'master' into nymkappa/feature/config-automatic-block-reindex 2022-07-10 12:10:38 +02:00
wiz
4fa4088694 Merge pull request #2033 from mempool/nymkappa/bugfix/cleanup-hashrate-indexing
Fix hashrate indexing, log difficulty adjustment progress
2022-07-10 12:06:34 +02:00
nymkappa
07cb4a49bc Index weekly mining pool hashrate only if there are blocks mined 2022-07-09 22:34:18 +02:00
nymkappa
067ee168dd Use oldest consecutive block timestamp as hashrate indexing limit 2022-07-09 22:11:27 +02:00
nymkappa
d8a90cce47 Use raw db hashrate instead of avg for indexing 2022-07-09 22:11:27 +02:00
nymkappa
e303a4c374 Removed hardcoded genesis timestamp
Fix duplicated genesis hashrate attempt
Add log during difficulty adjustment indexing
2022-07-09 22:11:26 +02:00
wiz
c75f485e54 Merge pull request #1999 from mempool/nymkappa/bugfix/schedule-indexing-updates
Run the mining indexer every hour to index missing/updated data
2022-07-09 20:07:21 +02:00
nymkappa
f3f0c688d8 Disable automatic block re-indexing by default 2022-07-09 19:04:35 +02:00
nymkappa
030020ea9e Run the mining indexer every hour to index missing/updated data 2022-07-09 18:25:16 +02:00
nymkappa
682682c74a Ignore prices without USD exchange rate 2022-07-09 16:13:01 +02:00
nymkappa
b5daf205a0 Fix pagination issue in blocks list 2022-07-09 16:02:43 +02:00
wiz
1037fbe52b Merge pull request #2041 from Emzy/ops/freebsd-nginx-conf
Install nginx config also for freebsd on prod install
2022-07-09 14:33:24 +02:00
wiz
051d151fb7 Merge pull request #2002 from mempool/nymkappa/feature/automatic-block-reindexing
Automatic block re-indexing upon pools.json update
2022-07-09 13:28:44 +02:00
Felipe Knorr Kuhn
7ba7440bb6 Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-09 00:04:35 +02:00
wiz
81f20e53ea Merge pull request #2043 from mempool/translations_frontend-src-locale-messages-xlf--master_nb
Translate '/frontend/src/locale/messages.xlf' in 'nb'
2022-07-08 18:56:09 +02:00
wiz
05a2c05a9e Merge pull request #2044 from mempool/translations_frontend-src-locale-messages-xlf--master_fr
Translate '/frontend/src/locale/messages.xlf' in 'fr'
2022-07-08 18:55:44 +02:00
wiz
44b1daeed2 Merge pull request #2046 from knorrium/knorrium/fix-package-lock
Fix missing dependencies
2022-07-08 18:55:17 +02:00
Felipe Knorr Kuhn
bed266abac Fix missing dependencies 2022-07-08 09:40:14 -07:00
wiz
8487548271 Merge pull request #2045 from mempool/wiz/update-nodejs-v16.16.0
Update to Node.js v16.16.0, use shared zlib for prod
2022-07-08 18:33:06 +02:00
wiz
c5e8a83ebb Update to Node.js v16.16.0, use shared zlib for prod 2022-07-08 17:22:56 +02:00
transifex-integration[bot]
b6f81bc83a Translate /frontend/src/locale/messages.xlf in nb
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'nb' language.
2022-07-08 15:07:38 +00:00
transifex-integration[bot]
294c278c42 Translate /frontend/src/locale/messages.xlf in fr
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'fr' language.
2022-07-08 15:07:38 +00:00
wiz
ff88a65936 Merge pull request #2036 from mempool/simon/amount-string-merge
Renaming value -> amount
2022-07-08 17:07:23 +02:00
wiz
76c7508224 Merge pull request #2037 from knorrium/knorrium/node_matrix
Run the CI action on 16 and 18
2022-07-08 17:06:51 +02:00
wiz
3c02131133 Merge pull request #2042 from mempool/simon/revert-angular-14-upgrade
Revert: Angular 14 upgrade
2022-07-08 17:03:49 +02:00
wiz
c89fd8c39f Merge pull request #2040 from Emzy/ops/disable-dialog
Remove the dialog for now on prod install
2022-07-08 16:55:40 +02:00
wiz
69623d71b2 Merge pull request #2039 from mempool/nymkappa/bugfix/use-core-for-block-list
Use bitcoin core to fetch blocks
2022-07-08 16:50:34 +02:00
softsimon
2d6f4d3bdb Revert: Angular 14 upgrade 2022-07-08 16:45:24 +02:00
nymkappa
97ff1e37aa Use bitcoin core instead of esplore for fetch blocks on bitcoin networks 2022-07-08 16:34:00 +02:00
Stephan Oeste
d0381e7850 Install nginx config also for freebsd on prod install 2022-07-08 15:59:39 +02:00
wiz
c4638f2ac5 Merge pull request #2035 from Emzy/ops/build-mempool
Add mempool build as final step on prod install
2022-07-08 15:34:50 +02:00
wiz
83c383b1ec Print onions on separate lines after prod install 2022-07-08 15:34:10 +02:00
Stephan Oeste
4f22864080 Add mempool build as final step on prod install 2022-07-08 15:29:59 +02:00
Stephan Oeste
92780daa78 Remove the dialog for now on prod install 2022-07-08 15:26:13 +02:00
Felipe Knorr Kuhn
a5e4b09e64 Set fail fast to false on the frontend jobs 2022-07-08 05:13:26 -07:00
Felipe Knorr Kuhn
1501dd23ab Continue running CI jobs if something fails 2022-07-08 04:49:51 -07:00
Felipe Knorr Kuhn
15ab134fa4 Run the CI action on 16 and 18 2022-07-08 04:44:19 -07:00
softsimon
05f0ba72e2 Renaming value -> amount 2022-07-08 13:31:10 +02:00
wiz
c9c5e8008c Merge pull request #2030 from mempool/simon/angular-14-upgrade
Angular 14 Upgrade
2022-07-08 12:10:10 +02:00
wiz
bdd3af6b6a Merge pull request #2032 from Emzy/ops/fix-elements-service
Fix service name to elements and add elements testnet for prod install
2022-07-08 11:57:36 +02:00
Stephan Oeste
6582c8b36f Fix service name to elements and add elements testnet for prod install 2022-07-08 11:54:26 +02:00
wiz
be838ec313 Merge pull request #2031 from Emzy/ops/fix-echo
Quote echo output in prod install
2022-07-08 11:33:03 +02:00
Stephan Oeste
92eef3a6c1 Quote echo output in prod install 2022-07-08 11:05:45 +02:00
softsimon
63939981c1 Angular 14 Upgrade 2022-07-08 10:44:07 +02:00
wiz
c9f788e3a4 Merge pull request #2012 from Emzy/ops/nginx-linux-patch
Installing Linux nginx config in prod install
2022-07-07 22:52:16 +02:00
wiz
0a866b468a Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 22:41:26 +02:00
wiz
8f0f755014 Merge pull request #2020 from mempool/nymkappa/bugfix/duplicate-genesis-hashrate-indexing
[Hashrate indexing] - Signet started in 2020 and not in 2009
2022-07-07 22:41:07 +02:00
nymkappa
5943b88ffe [Hashrate indexing] - Signet started in 2020 and not in 2009 2022-07-07 21:55:28 +02:00
wiz
c5dfe92e60 Merge pull request #2017 from mempool/translations_frontend-src-locale-messages-xlf--master_nb
Translate '/frontend/src/locale/messages.xlf' in 'nb'
2022-07-07 21:52:13 +02:00
wiz
753cf3cbac Merge pull request #2018 from mempool/translations_frontend-src-locale-messages-xlf--master_fr
Translate '/frontend/src/locale/messages.xlf' in 'fr'
2022-07-07 21:52:04 +02:00
wiz
4a64984d7f Merge pull request #2014 from mempool/wiz/cleanup-package-lock
Remove unused deps from backend/package-lock.json
2022-07-07 21:13:50 +02:00
Stephan Oeste
eeb84e5d42 Installing Linux nginx config in prod install 2022-07-07 21:09:00 +02:00
transifex-integration[bot]
50cd8c80d8 Translate /frontend/src/locale/messages.xlf in fr
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'fr' language.
2022-07-07 18:55:27 +00:00
wiz
567d4aebbc Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 20:55:08 +02:00
Felipe Knorr Kuhn
b53cc4c37c Merge branch 'master' into wiz/cleanup-package-lock 2022-07-07 11:41:59 -07:00
wiz
d46e1abd07 Merge pull request #2011 from mempool/wiz/fix-npm-install-no-optional
Fix npm install commands in Dockerfiles and ops scripts
2022-07-07 20:37:34 +02:00
wiz
0b0c0b458f Merge branch 'master' into wiz/fix-npm-install-no-optional 2022-07-07 20:36:58 +02:00
wiz
997b5a1c9d Merge pull request #2015 from mempool/simon/adding-dropdown-component-to-project
Moving ngx-bootrap-multiselect to the project
2022-07-07 20:36:35 +02:00
transifex-integration[bot]
4345661a0b Translate /frontend/src/locale/messages.xlf in nb
review completed for the source file '/frontend/src/locale/messages.xlf'
on the 'nb' language.
2022-07-07 18:30:31 +00:00
softsimon
5867c79a1f Moving ngx-bootrap-multiselect to the project 2022-07-07 20:14:58 +02:00
wiz
41f0619572 Merge pull request #2013 from Emzy/ops/set-user-ulimit
Set ulimit highter for all users in prod install
2022-07-07 19:56:38 +02:00
wiz
96b4ea6b50 Merge pull request #1998 from Emzy/ops/fix-linux-crontab
Make user crontab reliable in prod install script
2022-07-07 19:56:06 +02:00
wiz
8dda51a92a Remove unused deps from backend/package-lock.json 2022-07-07 19:33:43 +02:00
Stephan Oeste
b4bb54212c Set ulimit highter for all users in prod install 2022-07-07 19:22:47 +02:00
wiz
d57193c269 Fix npm install commands in Dockerfiles and ops scripts 2022-07-07 18:53:17 +02:00
wiz
4bc03c2d60 Merge branch 'master' into nymkappa/feature/automatic-block-reindexing 2022-07-07 18:21:04 +02:00
Stephan Oeste
bf969ec8f7 Make user crontab reliable in prod install script 2022-07-07 17:13:09 +02:00
wiz
6ead907e08 Merge pull request #1953 from mononaut/breathe-effect-framerate
limit pulsing blocks animation frame rate to 30FPS
2022-07-07 16:56:18 +02:00
wiz
243168a450 Merge branch 'master' into breathe-effect-framerate 2022-07-07 16:43:03 +02:00
wiz
06d2cf1b88 Merge pull request #2010 from mempool/simon/transifex-pull-0707
Pulled from Transifex
2022-07-07 16:42:24 +02:00
softsimon
d622162f33 Pulled from Transifex 2022-07-07 16:40:18 +02:00
wiz
12807583c2 Merge pull request #2003 from mempool/ops/fix-npm-install-prod
Add `--prod` to `npm install` in ops scripts
2022-07-07 16:36:11 +02:00
wiz
47f3d539c3 Merge pull request #2004 from mempool/nymkappa/bugfix/jumping-pagination
Set pagination font to monospace
2022-07-07 16:35:29 +02:00
wiz
993cd64126 Merge pull request #2008 from mempool/wiz/fix-mononaut-tooltip-width
Increase width of mononaut transaction tooltip
2022-07-07 16:34:56 +02:00
softsimon
a0e32ab0bd Auto resize block overview tooltip 2022-07-07 16:25:26 +02:00
wiz
409763b885 Merge pull request #2009 from mempool/simon/bisq-graphs-resize
Auto resize graphs on window resize
2022-07-07 15:58:55 +02:00
softsimon
e06819fc6f Auto resize graphs on window resize
fixes #1607
2022-07-07 15:46:13 +02:00
wiz
812783f2cd Increase width of mononaut transaction tooltip
Fixes #2007
2022-07-07 15:43:03 +02:00
Felipe Knorr Kuhn
849373a6d3 Merge branch 'master' into breathe-effect-framerate 2022-07-07 06:36:17 -07:00
nymkappa
5de559f5ad Set pagination font to monospace 2022-07-07 14:00:54 +02:00
wiz
bc068a0d9a Add --prod to npm install in ops scripts 2022-07-07 13:59:51 +02:00
nymkappa
4723a9d41b Re-index related blocks when mining pool.json changes 2022-07-07 13:41:09 +02:00
wiz
f9dfbf94ef Merge branch 'master' into breathe-effect-framerate 2022-07-06 23:45:29 +02:00
wiz
c7014fc6c8 Merge branch 'master' into breathe-effect-framerate 2022-07-06 01:24:59 +02:00
Felipe Knorr Kuhn
083634826e Merge branch 'master' into breathe-effect-framerate 2022-07-04 09:10:56 -07:00
Mononaut
2c73153db0 limit pulsing blocks animation frame rate to 30FPS
possibly resolves #1941
2022-06-29 16:09:04 +00:00
81 changed files with 17453 additions and 10875 deletions

View File

@@ -2,91 +2,93 @@ name: CI Pipeline for the Backend and Frontend
on: on:
pull_request: pull_request:
types: [ opened, review_requested, synchronize ] types: [opened, review_requested, synchronize]
env:
NODE_VERSION: 16.15.0
jobs: jobs:
backend: backend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy: strategy:
matrix: matrix:
flavor: ['dev', 'prod'] node: ["16.16.0", "18.5.0"]
runs-on: 'ubuntu-latest' flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
name: Backend (${{ matrix.flavor }}) name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
path: ${{ matrix.flavor }} path: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ matrix.node }}
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- name: Install - name: Install
if: ${{ matrix.flavor == 'dev'}} if: ${{ matrix.flavor == 'dev'}}
run: npm ci run: npm ci
working-directory: ${{ matrix.flavor }}/backend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Install (Prod dependencies only) - name: Install (Prod dependencies only)
if: ${{ matrix.flavor == 'prod'}} if: ${{ matrix.flavor == 'prod'}}
run: npm ci --prod --no-optional run: npm ci --omit=dev --omit=optional
working-directory: ${{ matrix.flavor }}/backend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
- name: Lint - name: Lint
if: ${{ matrix.flavor == 'dev'}} if: ${{ matrix.flavor == 'dev'}}
run: npm run lint run: npm run lint
working-directory: ${{ matrix.flavor }}/backend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
# - name: Test # - name: Test
# run: npm run test # run: npm run test
- name: Build - name: Build
run: npm run build run: npm run build
working-directory: ${{ matrix.flavor }}/backend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
frontend: frontend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
strategy: strategy:
matrix: matrix:
flavor: ['dev', 'prod'] node: ["16.15.0", "18.5.0"]
runs-on: 'ubuntu-latest' flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
name: Frontend (${{ matrix.flavor }}) name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
path: ${{ matrix.flavor }} path: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ matrix.node }}
registry-url: 'https://registry.npmjs.org' registry-url: "https://registry.npmjs.org"
- name: Install (Prod dependencies only) - name: Install (Prod dependencies only)
run: npm ci --prod --no-optional run: npm ci --omit=dev --omit=optional
if: ${{ matrix.flavor == 'prod'}} if: ${{ matrix.flavor == 'prod'}}
working-directory: ${{ matrix.flavor }}/frontend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
- name: Install - name: Install
if: ${{ matrix.flavor == 'dev'}} if: ${{ matrix.flavor == 'dev'}}
run: npm ci run: npm ci
working-directory: ${{ matrix.flavor }}/frontend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
- name: Lint - name: Lint
if: ${{ matrix.flavor == 'dev'}} if: ${{ matrix.flavor == 'dev'}}
run: npm run lint run: npm run lint
working-directory: ${{ matrix.flavor }}/frontend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
# - name: Test # - name: Test
# run: npm run test # run: npm run test
- name: Build - name: Build
run: npm run build run: npm run build
working-directory: ${{ matrix.flavor }}/frontend working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend

2
.nvmrc
View File

@@ -1 +1 @@
v16.15.0 v16.16.0

View File

@@ -20,7 +20,8 @@
"EXTERNAL_MAX_RETRY": 1, "EXTERNAL_MAX_RETRY": 1,
"EXTERNAL_RETRY_INTERVAL": 0, "EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool", "USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug" "STDOUT_LOG_MIN_PRIORITY": "debug",
"AUTOMATIC_BLOCK_REINDEXING": false
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View File

@@ -1,12 +1,12 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.4.1-dev", "version": "2.4.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.4.1-dev", "version": "2.4.1",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@mempool/electrum-client": "^1.1.7", "@mempool/electrum-client": "^1.1.7",
@@ -33,32 +33,6 @@
"prettier": "^2.7.1" "prettier": "^2.7.1"
} }
}, },
"node_modules/@babel/code-frame": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
"dev": true
},
"node_modules/@babel/highlight": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.10.4",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mempool-backend", "name": "mempool-backend",
"version": "2.4.1-dev", "version": "2.4.1",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",

View File

@@ -168,7 +168,7 @@ class Blocks {
blockExtended.extras.avgFeeRate = stats.avgfeerate; blockExtended.extras.avgFeeRate = stats.avgfeerate;
} }
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag; let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) { if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx); pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
@@ -280,8 +280,7 @@ class Blocks {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100; const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
const timeLeft = Math.round((indexedBlocks.length - totalIndexed) / blockPerSeconds); logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
} }
@@ -293,7 +292,11 @@ class Blocks {
totalIndexed++; totalIndexed++;
newlyIndexed++; newlyIndexed++;
} }
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); if (newlyIndexed > 0) {
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
} else {
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
}
} catch (e) { } catch (e) {
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e; throw e;
@@ -348,8 +351,7 @@ class Blocks {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds); const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100; const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds); logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false); loadingIndicators.setProgress('block-indexing', progress, false);
@@ -365,7 +367,11 @@ class Blocks {
currentBlockHeight -= chunkSize; currentBlockHeight -= chunkSize;
} }
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`); if (newlyIndexed > 0) {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
} else {
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`);
}
loadingIndicators.setProgress('block-indexing', 100); loadingIndicators.setProgress('block-indexing', 100);
} catch (e) { } catch (e) {
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
@@ -405,7 +411,7 @@ class Blocks {
if (blockHeightTip >= 2016) { if (blockHeightTip >= 2016) {
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016); const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash); const previousPeriodBlock = await bitcoinClient.getBlock(previousPeriodBlockHash)
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100; this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
logger.debug(`Initial difficulty adjustment data set.`); logger.debug(`Initial difficulty adjustment data set.`);
} }
@@ -527,13 +533,15 @@ class Blocks {
} }
} }
const block = await bitcoinApi.$getBlock(hash); let block = await bitcoinClient.getBlock(hash);
// Not Bitcoin network, return the block as it // Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return block; return block;
} }
block = prepareBlock(block);
// Bitcoin network, add our custom data on top // Bitcoin network, add our custom data on top
const transactions = await this.$getTransactionsExtended(hash, block.height, true); const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions); const blockExtended = await this.$getBlockExtended(block, transactions);
@@ -577,47 +585,39 @@ class Blocks {
} }
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> { public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
try { let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight(); const returnBlocks: BlockExtended[] = [];
const returnBlocks: BlockExtended[] = [];
if (currentHeight < 0) {
return returnBlocks;
}
if (currentHeight === 0 && Common.indexingEnabled()) {
currentHeight = await blocksRepository.$mostRecentBlockHeight();
}
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
currentHeight--;
}
if (currentHeight < 0) {
return returnBlocks; return returnBlocks;
} catch (e) {
throw e;
} }
// Check if block height exist in local cache to skip the hash lookup
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
let startFromHash: string | null = null;
if (blockByHeight) {
startFromHash = blockByHeight.id;
} else if (!Common.indexingEnabled()) {
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
}
let nextHash = startFromHash;
for (let i = 0; i < limit && currentHeight >= 0; i++) {
let block = this.getBlocks().find((b) => b.height === currentHeight);
if (block) {
returnBlocks.push(block);
} else if (Common.indexingEnabled()) {
block = await this.$indexBlock(currentHeight);
returnBlocks.push(block);
} else if (nextHash != null) {
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
nextHash = block.previousblockhash;
returnBlocks.push(block);
}
currentHeight--;
}
return returnBlocks;
} }
public getLastDifficultyAdjustmentTime(): number { public getLastDifficultyAdjustmentTime(): number {

View File

@@ -173,26 +173,25 @@ class Mining {
*/ */
public async $generatePoolHashrateHistory(): Promise<void> { public async $generatePoolHashrateHistory(): Promise<void> {
const now = new Date(); const now = new Date();
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
try { // Run only if:
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing'); // * lastestRunDate is set to 0 (node backend restart, reorg)
// * we started a new week (around Monday midnight)
// Run only if: const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
// * lastestRunDate is set to 0 (node backend restart, reorg) if (!runIndexing) {
// * we started a new week (around Monday midnight) return;
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
if (!runIndexing) {
return;
}
} catch (e) {
throw e;
} }
try { try {
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps(); const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
const hashrates: any[] = []; const hashrates: any[] = [];
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7)); const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
const lastMondayMidnight = this.getDateMidnight(lastMonday); const lastMondayMidnight = this.getDateMidnight(lastMonday);
let toTimestamp = lastMondayMidnight.getTime(); let toTimestamp = lastMondayMidnight.getTime();
@@ -207,7 +206,7 @@ class Mining {
logger.debug(`Indexing weekly mining pool hashrate`); logger.debug(`Indexing weekly mining pool hashrate`);
loadingIndicators.setProgress('weekly-hashrate-indexing', 0); loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
const fromTimestamp = toTimestamp - 604800000; const fromTimestamp = toTimestamp - 604800000;
// Skip already indexed weeks // Skip already indexed weeks
@@ -217,14 +216,6 @@ class Mining {
continue; continue;
} }
// Check if we have blocks for the previous week (which mean that the week
// we are currently indexing has complete data)
const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000);
if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000); null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
@@ -232,34 +223,35 @@ class Mining {
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000); let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
pools = pools.map((pool: any) => { if (totalBlocks > 0) {
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; pools = pools.map((pool: any) => {
pool.share = (pool.blockCount / totalBlocks); pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
return pool; pool.share = (pool.blockCount / totalBlocks);
}); return pool;
for (const pool of pools) {
hashrates.push({
hashrateTimestamp: toTimestamp / 1000,
avgHashrate: pool['hashrate'],
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
}); });
}
newlyIndexed += hashrates.length; for (const pool of pools) {
await HashratesRepository.$saveHashrates(hashrates); hashrates.push({
hashrates.length = 0; hashrateTimestamp: toTimestamp / 1000,
avgHashrate: pool['hashrate'] ,
poolId: pool.poolId,
share: pool['share'],
type: 'weekly',
});
}
newlyIndexed += hashrates.length;
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) { if (elapsedSeconds > 1) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`); logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false); loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
@@ -272,6 +264,8 @@ class Mining {
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate()); await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`); logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
} else {
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
} }
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
@@ -285,20 +279,19 @@ class Mining {
* [INDEXING] Generate daily hashrate data * [INDEXING] Generate daily hashrate data
*/ */
public async $generateNetworkHashrateHistory(): Promise<void> { public async $generateNetworkHashrateHistory(): Promise<void> {
try { // We only run this once a day around midnight
// We only run this once a day around midnight const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing'); const now = new Date().getUTCDate();
const now = new Date().getUTCDate(); if (now === latestRunDate) {
if (now === latestRunDate) { return;
return;
}
} catch (e) {
throw e;
} }
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
try { try {
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp); const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f const genesisTimestamp = genesisBlock.time * 1000;
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
const lastMidnight = this.getDateMidnight(new Date()); const lastMidnight = this.getDateMidnight(new Date());
let toTimestamp = Math.round(lastMidnight.getTime()); let toTimestamp = Math.round(lastMidnight.getTime());
const hashrates: any[] = []; const hashrates: any[] = [];
@@ -313,7 +306,7 @@ class Mining {
logger.debug(`Indexing daily network hashrate`); logger.debug(`Indexing daily network hashrate`);
loadingIndicators.setProgress('daily-hashrate-indexing', 0); loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) { while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
const fromTimestamp = toTimestamp - 86400000; const fromTimestamp = toTimestamp - 86400000;
// Skip already indexed days // Skip already indexed days
@@ -323,17 +316,9 @@ class Mining {
continue; continue;
} }
// Check if we have blocks for the previous day (which mean that the day
// we are currently indexing has complete data)
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
if (blockStatsPreviousDay.blockCount === 0 && config.MEMPOOL.NETWORK === 'mainnet') { // We are done indexing
break;
}
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, fromTimestamp / 1000, toTimestamp / 1000); null, fromTimestamp / 1000, toTimestamp / 1000);
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, const lastBlockHashrate = blockStats.blockCount === 0 ? 0 : await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
blockStats.lastBlockHeight); blockStats.lastBlockHeight);
hashrates.push({ hashrates.push({
@@ -355,9 +340,8 @@ class Mining {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt)); const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds)); const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100; const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString(); const formattedDate = new Date(fromTimestamp).toUTCString();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`); logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
timer = new Date().getTime() / 1000; timer = new Date().getTime() / 1000;
indexedThisRun = 0; indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress); loadingIndicators.setProgress('daily-hashrate-indexing', progress);
@@ -369,7 +353,7 @@ class Mining {
} }
// Add genesis block manually // Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) { if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && !indexedTimestamp.includes(genesisTimestamp / 1000)) {
hashrates.push({ hashrates.push({
hashrateTimestamp: genesisTimestamp / 1000, hashrateTimestamp: genesisTimestamp / 1000,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1), avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
@@ -385,6 +369,8 @@ class Mining {
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate()); await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
if (newlyIndexed > 0) { if (newlyIndexed > 0) {
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`); logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
} else {
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
} }
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
@@ -405,27 +391,37 @@ class Mining {
} }
const blocks: any = await BlocksRepository.$getBlocksDifficulty(); const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
let currentDifficulty = 0; let currentDifficulty = genesisBlock.difficulty;
let totalIndexed = 0; let totalIndexed = 0;
if (indexedHeights[0] === false) { if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
await DifficultyAdjustmentsRepository.$saveAdjustments({ await DifficultyAdjustmentsRepository.$saveAdjustments({
time: 1231006505, time: genesisBlock.time,
height: 0, height: 0,
difficulty: 1.0, difficulty: currentDifficulty,
adjustment: 0.0, adjustment: 0.0,
}); });
} }
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
currentDifficulty = oldestConsecutiveBlock.difficulty;
}
let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000;
for (const block of blocks) { for (const block of blocks) {
if (block.difficulty !== currentDifficulty) { if (block.difficulty !== currentDifficulty) {
if (block.height === 0 || indexedHeights[block.height] === true) { // Already indexed if (indexedHeights[block.height] === true) { // Already indexed
currentDifficulty = block.difficulty; if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
}
continue; continue;
} }
let adjustment = block.difficulty / Math.max(1, currentDifficulty); let adjustment = block.difficulty / currentDifficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
await DifficultyAdjustmentsRepository.$saveAdjustments({ await DifficultyAdjustmentsRepository.$saveAdjustments({
@@ -436,12 +432,24 @@ class Mining {
}); });
totalIndexed++; totalIndexed++;
currentDifficulty = block.difficulty; if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
}
}
totalBlockChecked++;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5) {
const progress = Math.round(totalBlockChecked / blocks.length * 100);
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
timer = new Date().getTime() / 1000;
} }
} }
if (totalIndexed > 0) { if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`); logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
} else {
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`);
} }
} }

View File

@@ -1,6 +1,7 @@
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import config from '../config'; import config from '../config';
import BlocksRepository from '../repositories/BlocksRepository';
interface Pool { interface Pool {
name: string; name: string;
@@ -32,7 +33,6 @@ class PoolsParser {
// First we save every entries without paying attention to pool duplication // First we save every entries without paying attention to pool duplication
const poolsDuplicated: Pool[] = []; const poolsDuplicated: Pool[] = [];
logger.debug('Parse coinbase_tags');
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']); const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
for (let i = 0; i < coinbaseTags.length; ++i) { for (let i = 0; i < coinbaseTags.length; ++i) {
poolsDuplicated.push({ poolsDuplicated.push({
@@ -43,7 +43,6 @@ class PoolsParser {
'slug': '' 'slug': ''
}); });
} }
logger.debug('Parse payout_addresses');
const addressesTags = Object.entries(poolsJson['payout_addresses']); const addressesTags = Object.entries(poolsJson['payout_addresses']);
for (let i = 0; i < addressesTags.length; ++i) { for (let i = 0; i < addressesTags.length; ++i) {
poolsDuplicated.push({ poolsDuplicated.push({
@@ -56,7 +55,6 @@ class PoolsParser {
} }
// Then, we find unique mining pool names // Then, we find unique mining pool names
logger.debug('Identify unique mining pools');
const poolNames: string[] = []; const poolNames: string[] = [];
for (let i = 0; i < poolsDuplicated.length; ++i) { for (let i = 0; i < poolsDuplicated.length; ++i) {
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) { if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
@@ -119,8 +117,15 @@ class PoolsParser {
'slug': slug 'slug': slug
}; };
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) { const existingPool = existingPools.find((pool) => pool.name === poolNames[i]);
finalPoolDataUpdate.push(poolObj); if (existingPool !== undefined) {
// Check if any data was actually updated
const equals = (a, b) =>
a.length === b.length &&
a.every((v, i) => v === b[i]);
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
finalPoolDataUpdate.push(poolObj);
}
} else { } else {
logger.debug(`Add '${finalPoolName}' mining pool`); logger.debug(`Add '${finalPoolName}' mining pool`);
finalPoolDataAdd.push(poolObj); finalPoolDataAdd.push(poolObj);
@@ -140,40 +145,51 @@ class PoolsParser {
return; return;
} }
logger.debug(`Update pools table now`); if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {
logger.debug(`Update pools table now`);
// Add new mining pools into the database // Add new mining pools into the database
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES '; let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
for (let i = 0; i < finalPoolDataAdd.length; ++i) { for (let i = 0; i < finalPoolDataAdd.length; ++i) {
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}', queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}', '${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
${JSON.stringify(finalPoolDataAdd[i].slug)}),`; ${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
} }
queryAdd = queryAdd.slice(0, -1) + ';'; queryAdd = queryAdd.slice(0, -1) + ';';
// Updated existing mining pools in the database // Updated existing mining pools in the database
const updateQueries: string[] = []; const updateQueries: string[] = [];
for (let i = 0; i < finalPoolDataUpdate.length; ++i) { for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
updateQueries.push(` updateQueries.push(`
UPDATE pools UPDATE pools
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}', SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}', regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
slug='${finalPoolDataUpdate[i].slug}' slug='${finalPoolDataUpdate[i].slug}'
WHERE name='${finalPoolDataUpdate[i].name}' WHERE name='${finalPoolDataUpdate[i].name}'
;`); ;`);
}
try {
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
if (finalPoolDataAdd.length > 0) {
await DB.query({ sql: queryAdd, timeout: 120000 });
}
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool();
logger.info('Mining pools.json import completed');
} catch (e) {
logger.err(`Cannot import pools in the database`);
throw e;
}
} }
try { try {
if (finalPoolDataAdd.length > 0) {
await DB.query({ sql: queryAdd, timeout: 120000 });
}
for (const query of updateQueries) {
await DB.query({ sql: query, timeout: 120000 });
}
await this.insertUnknownPool(); await this.insertUnknownPool();
logger.info('Mining pools.json import completed');
} catch (e) { } catch (e) {
logger.err(`Cannot import pools in the database`); logger.err(`Cannot insert unknown pool in the database`);
throw e; throw e;
} }
} }
@@ -201,6 +217,36 @@ class PoolsParser {
logger.err('Unable to insert "Unknown" mining pool'); logger.err('Unable to insert "Unknown" mining pool');
} }
} }
/**
* Delete blocks which needs to be reindexed
*/
private async $deleteBlocskToReindex(finalPoolDataUpdate: any[]) {
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
return;
}
const blockCount = await BlocksRepository.$blockCount(null, null);
if (blockCount === 0) {
return;
}
for (const updatedPool of finalPoolDataUpdate) {
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
if (pool.length > 0) {
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`);
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
}
}
// Ignore early days of Bitcoin as there were not mining pool yet
logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing');
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
logger.notice('Truncating hashrates for future re-indexing');
await DB.query(`DELETE FROM hashrates`);
}
} }
export default new PoolsParser(); export default new PoolsParser();

View File

@@ -23,6 +23,7 @@ interface IConfig {
EXTERNAL_RETRY_INTERVAL: number; EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string; USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug'; STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
AUTOMATIC_BLOCK_REINDEXING: boolean;
}; };
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
@@ -113,6 +114,7 @@ const defaults: IConfig = {
'EXTERNAL_RETRY_INTERVAL': 0, 'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool', 'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug', 'STDOUT_LOG_MIN_PRIORITY': 'debug',
'AUTOMATIC_BLOCK_REINDEXING': false,
}, },
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',

View File

@@ -35,6 +35,8 @@ class Indexer {
this.runIndexer = false; this.runIndexer = false;
this.indexerRunning = true; this.indexerRunning = true;
logger.debug(`Running mining indexer`);
try { try {
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
@@ -54,9 +56,15 @@ class Indexer {
this.indexerRunning = false; this.indexerRunning = false;
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
setTimeout(() => this.reindex(), 10000); setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false;
return;
} }
this.indexerRunning = false; this.indexerRunning = false;
const runEvery = 1000 * 3600; // 1 hour
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
setTimeout(() => this.reindex(), runEvery);
} }
async $resetHashratesIndexingState() { async $resetHashratesIndexingState() {

View File

@@ -446,7 +446,7 @@ class BlocksRepository {
++idx; ++idx;
} }
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`); logger.debug(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
return true; return true;
} catch (e) { } catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
@@ -610,6 +610,24 @@ class BlocksRepository {
throw e; throw e;
} }
} }
/**
* Return the oldest block from a consecutive chain of block from the most recent one
*/
public async $getOldestConsecutiveBlock(): Promise<any> {
try {
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
for (let i = 0; i < rows.length - 1; ++i) {
if (rows[i].height - rows[i + 1].height > 1) {
return rows[i];
}
}
return rows[rows.length - 1];
} catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
} }
export default new BlocksRepository(); export default new BlocksRepository();

View File

@@ -46,9 +46,38 @@ class DifficultyAdjustmentsRepository {
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`; query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`;
if (descOrder === true) { if (descOrder === true) {
query += ` ORDER BY time DESC`; query += ` ORDER BY height DESC`;
} else { } else {
query += ` ORDER BY time`; query += ` ORDER BY height`;
}
try {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getRawAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
UNIX_TIMESTAMP(time) as time,
height as height,
difficulty as difficulty,
adjustment as adjustment
FROM difficulty_adjustments`;
if (interval) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
if (descOrder === true) {
query += ` ORDER BY height DESC`;
} else {
query += ` ORDER BY height`;
} }
try { try {

View File

@@ -1,6 +1,5 @@
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import { Common } from '../api/common'; import { Common } from '../api/common';
import config from '../config';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
@@ -30,6 +29,32 @@ class HashratesRepository {
} }
} }
public async $getRawNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
UNIX_TIMESTAMP(hashrate_timestamp) as timestamp,
avg_hashrate as avgHashrate
FROM hashrates`;
if (interval) {
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
AND hashrates.type = 'daily'`;
} else {
query += ` WHERE hashrates.type = 'daily'`;
}
query += ` ORDER by hashrate_timestamp`;
try {
const [rows]: any[] = await DB.query(query);
return rows;
} catch (e) {
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> { public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval); interval = Common.getSqlInterval(interval);

View File

@@ -4,6 +4,12 @@ import { Prices } from '../tasks/price-updater';
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: Prices): Promise<void> { public async $savePrices(time: number, prices: Prices): Promise<void> {
if (prices.USD === -1) {
// Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
// As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
return;
}
try { try {
await DB.query(` await DB.query(`
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY) INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
@@ -17,17 +23,17 @@ class PricesRepository {
} }
public async $getOldestPriceTime(): Promise<number> { public async $getOldestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices ORDER BY time LIMIT 1`); const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getLatestPriceTime(): Promise<number> { public async $getLatestPriceTime(): Promise<number> {
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices ORDER BY time DESC LIMIT 1`); const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
return oldestRow[0] ? oldestRow[0].time : 0; return oldestRow[0] ? oldestRow[0].time : 0;
} }
public async $getPricesTimes(): Promise<number[]> { public async $getPricesTimes(): Promise<number[]> {
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices`); const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1`);
return times.map(time => time.time); return times.map(time => time.time);
} }
} }

View File

@@ -734,7 +734,7 @@ class Routes {
public async $getDifficultyAdjustments(req: Request, res: Response) { public async $getDifficultyAdjustments(req: Request, res: Response) {
try { try {
const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, true); const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true);
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
@@ -790,7 +790,7 @@ class Routes {
public async getBlocks(req: Request, res: Response) { public async getBlocks(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await blocks.$getBlocks(height, 15)); res.json(await blocks.$getBlocks(height, 15));

View File

@@ -177,6 +177,8 @@ class PriceUpdater {
} }
if (insertedCount > 0) { if (insertedCount > 0) {
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`); logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
} else {
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
} }
// Insert Kraken weekly prices // Insert Kraken weekly prices
@@ -251,6 +253,8 @@ class PriceUpdater {
if (totalInserted > 0) { if (totalInserted > 0) {
logger.notice(`Inserted ${totalInserted} hourly historical prices into the db`); logger.notice(`Inserted ${totalInserted} hourly historical prices into the db`);
} else {
logger.debug(`Inserted ${totalInserted} hourly historical prices into the db`);
} }
} }
} }

View File

@@ -3,14 +3,14 @@ import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended { export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{ return <BlockExtended>{
id: block.id ?? block.hash, // hash for indexed block id: block.id ?? block.hash, // hash for indexed block
timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block timestamp: block.timestamp ?? block.time ?? block.blockTimestamp, // blockTimestamp for indexed block
height: block.height, height: block.height,
version: block.version, version: block.version,
bits: block.bits, bits: (typeof block.bits === 'string' ? parseInt(block.bits, 16): block.bits),
nonce: block.nonce, nonce: block.nonce,
difficulty: block.difficulty, difficulty: block.difficulty,
merkle_root: block.merkle_root, merkle_root: block.merkle_root ?? block.merkleroot,
tx_count: block.tx_count, tx_count: block.tx_count ?? block.nTx,
size: block.size, size: block.size,
weight: block.weight, weight: block.weight,
previousblockhash: block.previousblockhash, previousblockhash: block.previousblockhash,

View File

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

View File

@@ -20,7 +20,8 @@
"USER_AGENT": "__MEMPOOL_USER_AGENT__", "USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__", "STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__, "INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__ "BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__
}, },
"CORE_RPC": { "CORE_RPC": {
"HOST": "__CORE_RPC_HOST__", "HOST": "__CORE_RPC_HOST__",

View File

@@ -22,6 +22,8 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@@ -110,6 +112,8 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json

View File

@@ -1,4 +1,4 @@
FROM node:16.15.0-buster-slim AS builder FROM node:16.16.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash} ENV DOCKER_COMMIT_HASH=${commitHash}
@@ -8,7 +8,7 @@ WORKDIR /build
COPY . . COPY . .
RUN apt-get update RUN apt-get update
RUN apt-get install -y build-essential rsync RUN apt-get install -y build-essential rsync
RUN npm i RUN npm install --omit=dev --omit=optional
RUN npm run build RUN npm run build
FROM nginx:1.17.8-alpine FROM nginx:1.17.8-alpine

View File

@@ -1,12 +1,12 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.4.1-dev", "version": "2.4.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.4.1-dev", "version": "2.4.1",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"dependencies": { "dependencies": {
"@angular-devkit/build-angular": "~13.3.7", "@angular-devkit/build-angular": "~13.3.7",
@@ -36,7 +36,6 @@
"echarts": "~5.3.2", "echarts": "~5.3.2",
"express": "^4.17.1", "express": "^4.17.1",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1", "ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0", "qrcode": "1.5.0",
@@ -11042,6 +11041,12 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -12788,19 +12793,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
}, },
"node_modules/ngx-bootrap-multiselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
"integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": "^10.0.6",
"@angular/core": "^10.0.6",
"@angular/forms": "^10.0.6"
}
},
"node_modules/ngx-echarts": { "node_modules/ngx-echarts": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
@@ -13805,6 +13797,17 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/portfinder": { "node_modules/portfinder": {
"version": "1.0.28", "version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -26083,6 +26086,12 @@
"@sideway/pinpoint": "^2.0.0" "@sideway/pinpoint": "^2.0.0"
} }
}, },
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -27418,14 +27427,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
}, },
"ngx-bootrap-multiselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
"integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
"requires": {
"tslib": "^2.0.0"
}
},
"ngx-echarts": { "ngx-echarts": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
@@ -28187,6 +28188,12 @@
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"peer": true
},
"portfinder": { "portfinder": {
"version": "1.0.28", "version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "mempool-frontend", "name": "mempool-frontend",
"version": "2.4.1-dev", "version": "2.4.1",
"description": "Bitcoin mempool visualizer and blockchain explorer backend", "description": "Bitcoin mempool visualizer and blockchain explorer backend",
"license": "GNU Affero General Public License v3.0", "license": "GNU Affero General Public License v3.0",
"homepage": "https://mempool.space", "homepage": "https://mempool.space",
@@ -90,7 +90,6 @@
"echarts": "~5.3.2", "echarts": "~5.3.2",
"express": "^4.17.1", "express": "^4.17.1",
"lightweight-charts": "~3.8.0", "lightweight-charts": "~3.8.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1", "ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0", "qrcode": "1.5.0",

View File

@@ -3,7 +3,7 @@
<div class="d-block float-right" id="filter"> <div class="d-block float-right" id="filter">
<form [formGroup]="radioGroupForm"> <form [formGroup]="radioGroupForm">
<ngx-bootrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootrap-multiselect> <ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect>
</form> </form>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { FormGroup, FormBuilder } from '@angular/forms'; import { FormGroup, FormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect'; import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types'
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
@Component({ @Component({

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BisqRoutingModule } from './bisq.routing.module'; import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component'; import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component'; import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
@@ -24,6 +23,10 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component'; import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component'; import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -44,16 +47,21 @@ import { CommonModule } from '@angular/common';
BisqMarketComponent, BisqMarketComponent,
BisqTradesComponent, BisqTradesComponent,
BisqMainDashboardComponent, BisqMainDashboardComponent,
NgxDropdownMultiselectComponent,
AutofocusDirective,
OffClickDirective,
], ],
imports: [ imports: [
CommonModule, CommonModule,
BisqRoutingModule, BisqRoutingModule,
SharedModule, SharedModule,
FontAwesomeModule, FontAwesomeModule,
NgxBootstrapMultiselectModule,
], ],
providers: [ providers: [
BisqApiService, BisqApiService,
MultiSelectSearchFilter,
AutofocusDirective,
OffClickDirective,
] ]
}) })
export class BisqModule { export class BisqModule {

View File

@@ -1,5 +1,5 @@
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts'; import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({ @Component({
selector: 'app-lightweight-charts-area', selector: 'app-lightweight-charts-area',
@@ -25,6 +25,15 @@ export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDest
private element: ElementRef, private element: ElementRef,
) { } ) { }
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.width = this.element.nativeElement.parentElement.offsetWidth;
this.chart.applyOptions({
width: this.width,
height: this.height,
});
}
ngOnInit() { ngOnInit() {
this.width = this.element.nativeElement.parentElement.offsetWidth; this.width = this.element.nativeElement.parentElement.offsetWidth;
this.container = document.createElement('div'); this.container = document.createElement('div');

View File

@@ -1,5 +1,5 @@
import { createChart, CrosshairMode } from 'lightweight-charts'; import { createChart, CrosshairMode } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({ @Component({
selector: 'app-lightweight-charts', selector: 'app-lightweight-charts',
@@ -21,6 +21,14 @@ export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy
private element: ElementRef, private element: ElementRef,
) { } ) { }
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.chart.applyOptions({
width: this.element.nativeElement.parentElement.offsetWidth,
height: this.height,
});
}
ngOnInit() { ngOnInit() {
this.chart = createChart(this.element.nativeElement, { this.chart = createChart(this.element.nativeElement, {
width: this.element.nativeElement.parentElement.offsetWidth, width: this.element.nativeElement.parentElement.offsetWidth,

View File

@@ -9,13 +9,13 @@
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td i18n="shared.transaction">Transaction</td> <td class="td-width" i18n="shared.transaction">Transaction</td>
<td> <td>
<a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a> <a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td class="td-width" i18n="transaction.value|Transaction value">Value</td> <td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
<td><app-amount [satoshis]="value"></app-amount></td> <td><app-amount [satoshis]="value"></app-amount></td>
</tr> </tr>
<tr> <tr>
@@ -23,13 +23,13 @@
<td>{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> &nbsp; <span class="fiat"><app-fiat [value]="fee"></app-fiat></span></td> <td>{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> &nbsp; <span class="fiat"><app-fiat [value]="fee"></app-fiat></span></td>
</tr> </tr>
<tr> <tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td> <td class="td-width" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td> <td>
{{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> {{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> <td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (vsize | vbytes: 2)"></td> <td [innerHTML]="'&lrm;' + (vsize | vbytes: 2)"></td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -9,10 +9,14 @@
justify-content: space-between; justify-content: space-between;
padding: 10px 15px; padding: 10px 15px;
text-align: left; text-align: left;
width: 320px; min-width: 320px;
pointer-events: none; pointer-events: none;
&.clickable { &.clickable {
pointer-events: all; pointer-events: all;
} }
} }
.td-width {
padding-right: 10px;
}

View File

@@ -143,7 +143,7 @@ export class BlockPredictionGraphComponent implements OnInit {
boundaryGap: false, boundaryGap: false,
axisLine: { onZero: true }, axisLine: { onZero: true },
axisLabel: { axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)), formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10) * 1000),
align: 'center', align: 'center',
fontSize: 11, fontSize: 11,
lineHeight: 12, lineHeight: 12,

View File

@@ -114,7 +114,11 @@
} }
.flashing { .flashing {
animation: opacityPulse 2s ease-out; /* force compositing */
will-change: opacity;
transform: translateZ(0);
/* effective max frame rate = (#keyframes - 1) x steps / duration */
animation: opacityPulse 2s steps(30, end);
animation-iteration-count: infinite; animation-iteration-count: infinite;
opacity: 1; opacity: 1;
} }

View File

@@ -17,7 +17,11 @@
} }
.flashing { .flashing {
animation: opacityPulse 2s ease-out; /* force compositing */
will-change: opacity;
transform: translateZ(0);
/* effective max frame rate = (#keyframes - 1) x steps / duration */
animation: opacityPulse 2s steps(30, end);
animation-iteration-count: infinite; animation-iteration-count: infinite;
opacity: 1; opacity: 1;
} }

View File

@@ -0,0 +1,41 @@
import { Directive, ElementRef, Host, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
@Directive({
selector: '[ssAutofocus]'
})
export class AutofocusDirective implements OnInit, OnChanges {
/**
* Will set focus if set to falsy value or not set at all
*/
@Input() ssAutofocus: any;
get element(): { focus?: Function } {
return this.elemRef.nativeElement;
}
constructor(
@Host() private elemRef: ElementRef,
) { }
ngOnInit() {
this.focus();
}
ngOnChanges(changes: SimpleChanges) {
const ssAutofocusChange = changes.ssAutofocus;
if (ssAutofocusChange && !ssAutofocusChange.isFirstChange()) {
this.focus();
}
}
focus() {
if (this.ssAutofocus) {
return;
}
this.element.focus && this.element.focus();
}
}

View File

@@ -0,0 +1,48 @@
a {
outline: none !important;
}
.dropdown-inline {
display: inline-block;
}
.dropdown-toggle .caret {
margin-left: 4px;
white-space: nowrap;
display: inline-block;
}
.chunkydropdown-menu {
min-width: 20em;
}
.chunkyrow {
line-height: 2;
margin-left: 1em;
font-size: 2em;
}
.slider {
width:3.8em;
height:3.8em;
display:block;
-webkit-transition: all 0.125s linear;
-moz-transition: all 0.125s linear;
-o-transition: all 0.125s linear;
transition: all 0.125s linear;
margin-left: 0.125em;
margin-top: auto;
}
.slideron {
margin-left: 1.35em;
}
.content_wrapper{
display: table-cell;
vertical-align: middle;
}
.search-container {
padding: 0px 5px 5px 5px;
}

View File

@@ -0,0 +1,72 @@
<div *ngIf="options" class="dropdown" [ngClass]="settings.containerClasses" [class.open]="isVisible" (offClick)="clickedOutside()">
<button type="button" class="dropdown-toggle" [ngClass]="settings.buttonClasses" (click)="toggleDropdown($event)" [disabled]="disabled"
[ssAutofocus]="!focusBack">
{{ title }}
<span class="caret"></span>
</button>
<div #scroller *ngIf="isVisible" class="dropdown-menu" [ngClass]="{'chunkydropdown-menu': settings.checkedStyle == 'visual' }"
(scroll)="settings.isLazyLoad ? checkScrollPosition($event) : null" (wheel)="settings.stopScrollPropagation ? checkScrollPropagation($event, scroller) : null"
[class.pull-right]="settings.pullRight" [class.dropdown-menu-right]="settings.pullRight" [style.max-height]="settings.maxHeight"
style="display: block; height: auto; overflow-y: auto;" (keydown.tab)="focusItem(1, $event)" (keydown.shift.tab)="focusItem(-1, $event)">
<div class="input-group search-container" *ngIf="settings.enableSearch && (renderFilteredOptions.length > 1 || filterControl.value.length > 0)">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">
<i class="fa fa-search" aria-hidden="true"></i>
</span>
</div>
<input type="text" class="form-control" ssAutofocus [formControl]="filterControl" [placeholder]="texts.searchPlaceholder"
class="form-control">
<div class="input-group-append" *ngIf="filterControl.value.length>0">
<button class="btn btn-default btn-secondary" type="button" (click)="clearSearch($event)">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-check" *ngIf="settings.showCheckAll && !disabledSelection && renderFilteredOptions.length > 1"
(click)="checkAll()">
<span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-ok': settings.checkedStyle !== 'fontawesome','fa fa-check': settings.checkedStyle === 'fontawesome'}"></span></span>
{{ texts.checkAll }}
</a>
<a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-uncheck" *ngIf="settings.showUncheckAll && !disabledSelection && renderFilteredOptions.length > 1"
(click)="uncheckAll()">
<span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-remove': settings.checkedStyle !== 'fontawesome','fa fa-times': settings.checkedStyle === 'fontawesome'}"></span></span>
{{ texts.uncheckAll }}
</a>
<a *ngIf="settings.showCheckAll || settings.showUncheckAll" href="javascript:;" class="dropdown-divider divider"></a>
<a *ngIf="!renderItems" href="javascript:;" class="dropdown-item empty">{{ texts.searchNoRenderText }}</a>
<a *ngIf="renderItems && !renderFilteredOptions.length" href="javascript:;" class="dropdown-item empty">{{ texts.searchEmptyResult }}</a>
<a class="dropdown-item" href="javascript:;" *ngFor="let option of renderFilteredOptions; trackBy: trackById" [class.active]="isSelected(option)"
[ngStyle]="getItemStyle(option)" [ngClass]="option.classes" [class.dropdown-header]="option.isLabel" [ssAutofocus]="option !== focusedItem"
tabindex="-1" (click)="setSelected($event, option)" (keydown.space)="setSelected($event, option)" (keydown.enter)="setSelected($event, option)">
<span *ngIf="!option.isLabel; else label" role="menuitem" tabindex="-1" [style.padding-left]="this.parents.length>0&&this.parents.indexOf(option.id)<0&&'30px'"
[ngStyle]="getItemStyleSelectionDisabled()">
<ng-container [ngSwitch]="settings.checkedStyle">
<input *ngSwitchCase="'checkboxes'" type="checkbox" [checked]="isSelected(option)" (click)="preventCheckboxCheck($event, option)"
[disabled]="isCheckboxDisabled(option)" [ngStyle]="getItemStyleSelectionDisabled()" />
<span *ngSwitchCase="'glyphicon'" style="width: 16px;" class="glyphicon" [class.glyphicon-ok]="isSelected(option)" [class.glyphicon-lock]="isCheckboxDisabled(option)"></span>
<span *ngSwitchCase="'fontawesome'" style="width: 16px;display: inline-block;">
<span *ngIf="isSelected(option)"><i class="fa fa-check" aria-hidden="true"></i></span>
<span *ngIf="isCheckboxDisabled(option)"><i class="fa fa-lock" aria-hidden="true"></i></span>
</span>
<span *ngSwitchCase="'visual'" style="display:block;float:left; border-radius: 0.2em; border: 0.1em solid rgba(44, 44, 44, 0.63);background:rgba(0, 0, 0, 0.1);width: 5.5em;">
<div class="slider" [ngClass]="{'slideron': isSelected(option)}">
<img *ngIf="option.image != null" [src]="option.image" style="height: 100%; width: 100%; object-fit: contain" />
<div *ngIf="option.image == null" style="height: 100%; width: 100%;text-align: center; display: table; background-color:rgba(0, 0, 0, 0.74)">
<div class="content_wrapper">
<span style="font-size:3em;color:white" class="glyphicon glyphicon-eye-close"></span>
</div>
</div>
</div>
</span>
</ng-container>
<span [ngClass]="{'chunkyrow': settings.checkedStyle == 'visual' }" [class.disabled]="isCheckboxDisabled(option)" [ngClass]="settings.itemClasses"
[style.font-weight]="this.parents.indexOf(option.id)>=0?'bold':'normal'">
{{ option.name }}
</span>
</span>
<ng-template #label>
<span [class.disabled]="isCheckboxDisabled(option)">{{ option.name }}</span>
</ng-template>
</a>
</div>
</div>

View File

@@ -0,0 +1,710 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
EventEmitter,
forwardRef,
Input,
IterableDiffers,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALUE_ACCESSOR,
Validator,
} from '@angular/forms';
import { takeUntil } from 'rxjs/operators';
import { MultiSelectSearchFilter } from './search-filter.pipe';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types';
import { Subject, Observable } from 'rxjs';
const MULTISELECT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxDropdownMultiselectComponent),
multi: true,
};
// tslint:disable-next-line: no-conflicting-lifecycle
@Component({
selector: 'ngx-bootstrap-multiselect',
templateUrl: './ngx-bootstrap-multiselect.component.html',
styleUrls: ['./ngx-bootstrap-multiselect.component.css'],
providers: [MULTISELECT_VALUE_ACCESSOR, MultiSelectSearchFilter],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NgxDropdownMultiselectComponent implements OnInit,
OnChanges,
DoCheck,
OnDestroy,
ControlValueAccessor,
Validator {
private localIsVisible = false;
private workerDocClicked = false;
filterControl: FormControl = this.fb.control('');
@Input() options: Array<IMultiSelectOption>;
@Input() settings: IMultiSelectSettings;
@Input() texts: IMultiSelectTexts;
@Input() disabled = false;
@Input() disabledSelection = false;
@Input() searchFunction: (str: string) => RegExp = this._escapeRegExp;
@Output() selectionLimitReached = new EventEmitter();
@Output() dropdownClosed = new EventEmitter();
@Output() dropdownOpened = new EventEmitter();
@Output() added = new EventEmitter();
@Output() removed = new EventEmitter();
@Output() lazyLoad = new EventEmitter();
@Output() filter: Observable<string> = this.filterControl.valueChanges;
get focusBack(): boolean {
return this.settings.focusBack && this._focusBack;
}
destroyed$ = new Subject<any>();
filteredOptions: IMultiSelectOption[] = [];
lazyLoadOptions: IMultiSelectOption[] = [];
renderFilteredOptions: IMultiSelectOption[] = [];
model: any[] = [];
prevModel: any[] = [];
parents: any[];
title: string;
differ: any;
numSelected = 0;
set isVisible(val: boolean) {
this.localIsVisible = val;
this.workerDocClicked = val ? false : this.workerDocClicked;
}
get isVisible(): boolean {
return this.localIsVisible;
}
renderItems = true;
checkAllSearchRegister = new Set();
checkAllStatus = false;
loadedValueIds = [];
_focusBack = false;
focusedItem: IMultiSelectOption | undefined;
defaultSettings: IMultiSelectSettings = {
closeOnClickOutside: true,
pullRight: false,
enableSearch: false,
searchRenderLimit: 0,
searchRenderAfter: 1,
searchMaxLimit: 0,
searchMaxRenderedItems: 0,
checkedStyle: 'checkboxes',
buttonClasses: 'btn btn-primary dropdown-toggle',
containerClasses: 'dropdown-inline',
selectionLimit: 0,
minSelectionLimit: 0,
closeOnSelect: false,
autoUnselect: false,
showCheckAll: false,
showUncheckAll: false,
fixedTitle: false,
dynamicTitleMaxItems: 3,
maxHeight: '300px',
isLazyLoad: false,
stopScrollPropagation: false,
loadViewDistance: 1,
selectAddedValues: false,
ignoreLabels: false,
maintainSelectionOrderInTitle: false,
focusBack: true
};
defaultTexts: IMultiSelectTexts = {
checkAll: 'Select all',
uncheckAll: 'Unselect all',
checked: 'selected',
checkedPlural: 'selected',
searchPlaceholder: 'Search...',
searchEmptyResult: 'Nothing found...',
searchNoRenderText: 'Type in search box to see results...',
defaultTitle: 'Select',
allSelected: 'All selected',
};
get searchLimit(): number | undefined {
return this.settings.searchRenderLimit;
}
get searchRenderAfter(): number | undefined {
return this.settings.searchRenderAfter;
}
get searchLimitApplied(): boolean {
return this.searchLimit > 0 && this.options.length > this.searchLimit;
}
constructor(
private fb: FormBuilder,
private searchFilter: MultiSelectSearchFilter,
differs: IterableDiffers,
private cdRef: ChangeDetectorRef
) {
this.differ = differs.find([]).create(null);
this.settings = this.defaultSettings;
this.texts = this.defaultTexts;
}
clickedOutside(): void {
if (!this.isVisible || !this.settings.closeOnClickOutside) { return; }
this.isVisible = false;
this._focusBack = true;
this.dropdownClosed.emit();
}
getItemStyle(option: IMultiSelectOption): any {
const style = {};
if (!option.isLabel) {
style['cursor'] = 'pointer';
}
if (option.disabled) {
style['cursor'] = 'default';
}
}
getItemStyleSelectionDisabled(): any {
if (this.disabledSelection) {
return { cursor: 'default' };
}
}
ngOnInit(): void {
this.title = this.texts.defaultTitle || '';
this.filterControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => {
this.updateRenderItems();
if (this.settings.isLazyLoad) {
this.load();
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes['options']) {
this.options = this.options || [];
this.parents = this.options
.filter(option => typeof option.parentId === 'number')
.map(option => option.parentId);
this.updateRenderItems();
if (
this.settings.isLazyLoad &&
this.settings.selectAddedValues &&
this.loadedValueIds.length === 0
) {
this.loadedValueIds = this.loadedValueIds.concat(
changes.options.currentValue.map(value => value.id)
);
}
if (
this.settings.isLazyLoad &&
this.settings.selectAddedValues &&
changes.options.previousValue
) {
const addedValues = changes.options.currentValue.filter(
value => this.loadedValueIds.indexOf(value.id) === -1
);
this.loadedValueIds.concat(addedValues.map(value => value.id));
if (this.checkAllStatus) {
this.addChecks(addedValues);
} else if (this.checkAllSearchRegister.size > 0) {
this.checkAllSearchRegister.forEach((searchValue: string) =>
this.addChecks(this.applyFilters(addedValues, searchValue))
);
}
}
if (this.texts) {
this.updateTitle();
}
this.fireModelChange();
}
if (changes['settings']) {
this.settings = { ...this.defaultSettings, ...this.settings };
}
if (changes['texts']) {
this.texts = { ...this.defaultTexts, ...this.texts };
if (!changes['texts'].isFirstChange()) { this.updateTitle(); }
}
}
ngOnDestroy() {
this.destroyed$.next(false);
}
updateRenderItems() {
this.renderItems =
!this.searchLimitApplied ||
this.filterControl.value.length >= this.searchRenderAfter;
this.filteredOptions = this.applyFilters(
this.options,
this.settings.isLazyLoad ? '' : this.filterControl.value
);
this.renderFilteredOptions = this.renderItems ? this.filteredOptions : [];
this.focusedItem = undefined;
}
applyFilters(options: IMultiSelectOption[], value: string): IMultiSelectOption[] {
return this.searchFilter.transform(
options,
value,
this.settings.searchMaxLimit,
this.settings.searchMaxRenderedItems,
this.searchFunction
);
}
fireModelChange(): void {
if (this.model != this.prevModel) {
this.prevModel = this.model;
this.onModelChange(this.model);
this.onModelTouched();
this.cdRef.markForCheck();
}
}
onModelChange: Function = (_: any) => { };
onModelTouched: Function = () => { };
writeValue(value: any): void {
if (value !== undefined && value !== null) {
this.model = Array.isArray(value) ? value : [value];
this.ngDoCheck();
} else {
this.model = [];
}
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
ngDoCheck() {
const changes = this.differ.diff(this.model);
if (changes) {
this.updateNumSelected();
this.updateTitle();
}
}
validate(_c: AbstractControl): { [key: string]: any } {
if (this.model && this.model.length) {
return {
required: {
valid: false
}
};
}
if (this.options.filter(o => this.model.indexOf(o.id) && !o.disabled).length === 0) {
return {
selection: {
valid: false
}
};
}
return null;
}
registerOnValidatorChange(_fn: () => void): void {
throw new Error('Method not implemented.');
}
clearSearch(event: Event) {
this.maybeStopPropagation(event);
this.filterControl.setValue('');
}
toggleDropdown(e?: Event) {
if (this.isVisible) {
this._focusBack = true;
}
this.isVisible = !this.isVisible;
this.isVisible ? this.dropdownOpened.emit() : this.dropdownClosed.emit();
this.focusedItem = undefined;
}
closeDropdown(e?: Event) {
this.isVisible = true;
this.toggleDropdown(e);
}
isSelected(option: IMultiSelectOption): boolean {
return this.model && this.model.indexOf(option.id) > -1;
}
setSelected(_event: Event, option: IMultiSelectOption) {
if (option.isLabel) {
return;
}
if (option.disabled) {
return;
}
if (this.disabledSelection) {
return;
}
setTimeout(() => {
this.maybeStopPropagation(_event);
this.maybePreventDefault(_event);
const index = this.model.indexOf(option.id);
const isAtSelectionLimit =
this.settings.selectionLimit > 0 &&
this.model.length >= this.settings.selectionLimit;
const removeItem = (idx, id): void => {
this.model.splice(idx, 1);
this.removed.emit(id);
if (
this.settings.isLazyLoad &&
this.lazyLoadOptions.some(val => val.id === id)
) {
this.lazyLoadOptions.splice(
this.lazyLoadOptions.indexOf(
this.lazyLoadOptions.find(val => val.id === id)
),
1
);
}
};
if (index > -1) {
if (
this.settings.minSelectionLimit === undefined ||
this.numSelected > this.settings.minSelectionLimit
) {
removeItem(index, option.id);
}
const parentIndex =
option.parentId && this.model.indexOf(option.parentId);
if (parentIndex > -1) {
removeItem(parentIndex, option.parentId);
} else if (this.parents.indexOf(option.id) > -1) {
this.options
.filter(
child =>
this.model.indexOf(child.id) > -1 &&
child.parentId === option.id
)
.forEach(child =>
removeItem(this.model.indexOf(child.id), child.id)
);
}
} else if (isAtSelectionLimit && !this.settings.autoUnselect) {
this.selectionLimitReached.emit(this.model.length);
return;
} else {
const addItem = (id): void => {
this.model.push(id);
this.added.emit(id);
if (
this.settings.isLazyLoad &&
!this.lazyLoadOptions.some(val => val.id === id)
) {
this.lazyLoadOptions.push(option);
}
};
addItem(option.id);
if (!isAtSelectionLimit) {
if (option.parentId && !this.settings.ignoreLabels) {
const children = this.options.filter(
child =>
child.id !== option.id && child.parentId === option.parentId
);
if (children.every(child => this.model.indexOf(child.id) > -1)) {
addItem(option.parentId);
}
} else if (this.parents.indexOf(option.id) > -1) {
const children = this.options.filter(
child =>
this.model.indexOf(child.id) < 0 && child.parentId === option.id
);
children.forEach(child => addItem(child.id));
}
} else {
removeItem(0, this.model[0]);
}
}
if (this.settings.closeOnSelect) {
this.toggleDropdown();
}
this.model = this.model.slice();
this.fireModelChange();
}, 0)
}
updateNumSelected() {
this.numSelected =
this.model.filter(id => this.parents.indexOf(id) < 0).length || 0;
}
updateTitle() {
let numSelectedOptions = this.options.length;
if (this.settings.ignoreLabels) {
numSelectedOptions = this.options.filter(
(option: IMultiSelectOption) => !option.isLabel
).length;
}
if (this.numSelected === 0 || this.settings.fixedTitle) {
this.title = this.texts ? this.texts.defaultTitle : '';
} else if (
this.settings.displayAllSelectedText &&
this.model.length === numSelectedOptions
) {
this.title = this.texts ? this.texts.allSelected : '';
} else if (
this.settings.dynamicTitleMaxItems &&
this.settings.dynamicTitleMaxItems >= this.numSelected
) {
const useOptions =
this.settings.isLazyLoad && this.lazyLoadOptions.length
? this.lazyLoadOptions
: this.options;
let titleSelections: Array<IMultiSelectOption>;
if (this.settings.maintainSelectionOrderInTitle) {
const optionIds = useOptions.map((selectOption: IMultiSelectOption, idx: number) => selectOption.id);
titleSelections = this.model
.map((selectedId) => optionIds.indexOf(selectedId))
.filter((optionIndex) => optionIndex > -1)
.map((optionIndex) => useOptions[optionIndex]);
} else {
titleSelections = useOptions.filter((option: IMultiSelectOption) => this.model.indexOf(option.id) > -1);
}
this.title = titleSelections.map((option: IMultiSelectOption) => option.name).join(', ');
} else {
this.title =
this.numSelected +
' ' +
(this.numSelected === 1
? this.texts.checked
: this.texts.checkedPlural);
}
this.cdRef.markForCheck();
}
searchFilterApplied() {
return (
this.settings.enableSearch &&
this.filterControl.value &&
this.filterControl.value.length > 0
);
}
addChecks(options) {
const checkedOptions = options
.filter((option: IMultiSelectOption) => {
if (
!option.disabled &&
(
this.model.indexOf(option.id) === -1 &&
!(this.settings.ignoreLabels && option.isLabel)
)
) {
this.added.emit(option.id);
return true;
}
return false;
})
.map((option: IMultiSelectOption) => option.id);
this.model = this.model.concat(checkedOptions);
}
checkAll(): void {
if (!this.disabledSelection) {
this.addChecks(
!this.searchFilterApplied() ? this.options : this.filteredOptions
);
if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
if (this.searchFilterApplied() && !this.checkAllStatus) {
this.checkAllSearchRegister.add(this.filterControl.value);
} else {
this.checkAllSearchRegister.clear();
this.checkAllStatus = true;
}
this.load();
}
this.fireModelChange();
}
}
uncheckAll(): void {
if (!this.disabledSelection) {
const checkedOptions = this.model;
let unCheckedOptions = !this.searchFilterApplied()
? this.model
: this.filteredOptions.map((option: IMultiSelectOption) => option.id);
// set unchecked options only to the ones that were checked
unCheckedOptions = checkedOptions.filter(item => unCheckedOptions.indexOf(item) > -1);
this.model = this.model.filter((id: number) => {
if (
(unCheckedOptions.indexOf(id) < 0 &&
this.settings.minSelectionLimit === undefined) ||
unCheckedOptions.indexOf(id) < this.settings.minSelectionLimit
) {
return true;
} else {
this.removed.emit(id);
return false;
}
});
if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
if (this.searchFilterApplied()) {
if (this.checkAllSearchRegister.has(this.filterControl.value)) {
this.checkAllSearchRegister.delete(this.filterControl.value);
this.checkAllSearchRegister.forEach(function(searchTerm) {
const filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.indexOf(option.id) > -1), searchTerm);
this.addChecks(filterOptions);
});
}
} else {
this.checkAllSearchRegister.clear();
this.checkAllStatus = false;
}
this.load();
}
this.fireModelChange();
}
}
preventCheckboxCheck(event: Event, option: IMultiSelectOption): void {
if (
option.disabled ||
(
this.settings.selectionLimit &&
!this.settings.autoUnselect &&
this.model.length >= this.settings.selectionLimit &&
this.model.indexOf(option.id) === -1 &&
this.maybePreventDefault(event)
)
) {
this.maybePreventDefault(event);
}
}
isCheckboxDisabled(option?: IMultiSelectOption): boolean {
return this.disabledSelection || option && option.disabled;
}
checkScrollPosition(ev): void {
const scrollTop = ev.target.scrollTop;
const scrollHeight = ev.target.scrollHeight;
const scrollElementHeight = ev.target.clientHeight;
const roundingPixel = 1;
const gutterPixel = 1;
if (
scrollTop >=
scrollHeight -
(1 + this.settings.loadViewDistance) * scrollElementHeight -
roundingPixel -
gutterPixel
) {
this.load();
}
}
checkScrollPropagation(ev, element): void {
const scrollTop = element.scrollTop;
const scrollHeight = element.scrollHeight;
const scrollElementHeight = element.clientHeight;
if (
(ev.deltaY > 0 && scrollTop + scrollElementHeight >= scrollHeight) ||
(ev.deltaY < 0 && scrollTop <= 0)
) {
ev = ev || window.event;
this.maybePreventDefault(ev);
ev.returnValue = false;
}
}
trackById(idx: number, selectOption: IMultiSelectOption): void {
return selectOption.id;
}
load(): void {
this.lazyLoad.emit({
length: this.options.length,
filter: this.filterControl.value,
checkAllSearches: this.checkAllSearchRegister,
checkAllStatus: this.checkAllStatus,
});
}
focusItem(dir: number, e?: Event): void {
if (!this.isVisible) {
return;
}
this.maybePreventDefault(e);
const idx = this.filteredOptions.indexOf(this.focusedItem);
if (idx === -1) {
this.focusedItem = this.filteredOptions[0];
return;
}
const nextIdx = idx + dir;
const newIdx =
nextIdx < 0
? this.filteredOptions.length - 1
: nextIdx % this.filteredOptions.length;
this.focusedItem = this.filteredOptions[newIdx];
}
private maybePreventDefault(e?: Event): void {
if (e && e.preventDefault) {
e.preventDefault();
}
}
private maybeStopPropagation(e?: Event): void {
if (e && e.stopPropagation) {
e.stopPropagation();
}
}
private _escapeRegExp(str: string): RegExp {
const regExpStr = str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
return new RegExp(regExpStr, 'i');
}
}

View File

@@ -0,0 +1,39 @@
import { Directive, HostListener } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { Output } from '@angular/core';
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[offClick]',
})
export class OffClickDirective {
@Output('offClick') onOffClick = new EventEmitter<any>();
private _clickEvent: MouseEvent;
private _touchEvent: TouchEvent;
@HostListener('click', ['$event'])
public onClick(event: MouseEvent): void {
this._clickEvent = event;
}
@HostListener('touchstart', ['$event'])
public onTouch(event: TouchEvent): void {
this._touchEvent = event;
}
@HostListener('document:click', ['$event'])
public onDocumentClick(event: MouseEvent): void {
if (event !== this._clickEvent) {
this.onOffClick.emit(event);
}
}
@HostListener('document:touchstart', ['$event'])
public onDocumentTouch(event: TouchEvent): void {
if (event !== this._touchEvent) {
this.onOffClick.emit(event);
}
}
}

View File

@@ -0,0 +1,130 @@
import { Pipe, PipeTransform } from '@angular/core';
import { IMultiSelectOption } from './types';
interface StringHashMap<T> {
[k: string]: T;
}
@Pipe({
name: 'searchFilter'
})
export class MultiSelectSearchFilter implements PipeTransform {
private _lastOptions: IMultiSelectOption[];
private _searchCache: StringHashMap<IMultiSelectOption[]> = {};
private _searchCacheInclusive: StringHashMap<boolean | number> = {};
private _prevSkippedItems: StringHashMap<number> = {};
transform(
options: IMultiSelectOption[],
str = '',
limit = 0,
renderLimit = 0,
searchFunction: (str: string) => RegExp,
): IMultiSelectOption[] {
str = str.toLowerCase();
// Drop cache because options were updated
if (options !== this._lastOptions) {
this._lastOptions = options;
this._searchCache = {};
this._searchCacheInclusive = {};
this._prevSkippedItems = {};
}
const filteredOpts = this._searchCache.hasOwnProperty(str)
? this._searchCache[str]
: this._doSearch(options, str, limit, searchFunction);
const isUnderLimit = options.length <= limit;
return isUnderLimit
? filteredOpts
: this._limitRenderedItems(filteredOpts, renderLimit);
}
private _getSubsetOptions(
options: IMultiSelectOption[],
prevOptions: IMultiSelectOption[],
prevSearchStr: string
) {
const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr];
if (prevInclusiveOrIdx === true) {
// If have previous results and it was inclusive, do only subsearch
return prevOptions;
} else if (typeof prevInclusiveOrIdx === 'number') {
// Or reuse prev results with unchecked ones
return [...prevOptions, ...options.slice(prevInclusiveOrIdx)];
}
return options;
}
private _doSearch(options: IMultiSelectOption[], str: string, limit: number, searchFunction: (str: string) => RegExp) {
const prevStr = str.slice(0, -1);
const prevResults = this._searchCache[prevStr];
const prevResultShift = this._prevSkippedItems[prevStr] || 0;
if (prevResults) {
options = this._getSubsetOptions(options, prevResults, prevStr);
}
const optsLength = options.length;
const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
const regexp = searchFunction(str);
const filteredOpts: IMultiSelectOption[] = [];
let i = 0, founded = 0, removedFromPrevResult = 0;
const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name);
const getChildren = (option: IMultiSelectOption) =>
options.filter(child => child.parentId === option.id);
const getParent = (option: IMultiSelectOption) =>
options.find(parent => option.parentId === parent.id);
const foundFn = (item: any) => { filteredOpts.push(item); founded++; };
const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { };
for (; i < optsLength && founded < maxFound; ++i) {
const option = options[i];
const directMatch = doesOptionMatch(option);
if (directMatch) {
foundFn(option);
continue;
}
if (typeof option.parentId === 'undefined') {
const childrenMatch = getChildren(option).some(doesOptionMatch);
if (childrenMatch) {
foundFn(option);
continue;
}
}
if (typeof option.parentId !== 'undefined') {
const parentMatch = doesOptionMatch(getParent(option));
if (parentMatch) {
foundFn(option);
continue;
}
}
notFoundFn();
}
const totalIterations = i + prevResultShift;
this._searchCache[str] = filteredOpts;
this._searchCacheInclusive[str] = i === optsLength || totalIterations;
this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift;
return filteredOpts;
}
private _limitRenderedItems<T>(items: T[], limit: number): T[] {
return items.length > limit && limit > 0 ? items.slice(0, limit) : items;
}
}

View File

@@ -0,0 +1,82 @@
export interface IMultiSelectOption {
id: any;
name: string;
disabled?: boolean;
isLabel?: boolean;
parentId?: any;
params?: any;
classes?: string;
image?: string;
}
export interface IMultiSelectSettings {
pullRight?: boolean;
enableSearch?: boolean;
closeOnClickOutside?: boolean;
/**
* 0 - By default
* If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit)
* then render items only when user typed more then or equal `searchRenderAfter` charachters
*/
searchRenderLimit?: number;
/**
* 3 - By default
*/
searchRenderAfter?: number;
/**
* 0 - By default
* If >0 will render only N first items
*/
searchMaxLimit?: number;
/**
* 0 - By default
* Used with searchMaxLimit to further limit rendering for optimization
* Should be less than searchMaxLimit to take effect
*/
searchMaxRenderedItems?: number;
checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome' | 'visual';
buttonClasses?: string;
itemClasses?: string;
containerClasses?: string;
selectionLimit?: number;
minSelectionLimit?: number;
closeOnSelect?: boolean;
autoUnselect?: boolean;
showCheckAll?: boolean;
showUncheckAll?: boolean;
fixedTitle?: boolean;
dynamicTitleMaxItems?: number;
maxHeight?: string;
displayAllSelectedText?: boolean;
isLazyLoad?: boolean;
loadViewDistance?: number;
stopScrollPropagation?: boolean;
selectAddedValues?: boolean;
/**
* false - By default
* If activated label IDs don't count and won't be written to the model.
*/
ignoreLabels?: boolean;
/**
* false - By default
* If activated, the title will show selections in the order they were selected.
*/
maintainSelectionOrderInTitle?: boolean;
/**
* @default true
* Set the focus back to the input control when the dropdown closed
*/
focusBack?: boolean;
}
export interface IMultiSelectTexts {
checkAll?: string;
uncheckAll?: string;
checked?: string;
checkedPlural?: string;
searchPlaceholder?: string;
searchEmptyResult?: string;
searchNoRenderText?: string;
defaultTitle?: string;
allSelected?: string;
}

View File

@@ -27,15 +27,7 @@ $width: 500;
$height: 500; $height: 500;
// Create the explosion... // Create the explosion...
$box-shadow: ();
$box-shadow2: ();
@for $i from 0 through $particles {
$box-shadow: $box-shadow,
random($width) - math.div($width, 1.2) + px
random($height) - math.div($height, 1.2) + px
hsl(random(360), 100%, 50%);
$box-shadow2: $box-shadow2, 0 0 #fff
}
@mixin keyframes ($animationName) { @mixin keyframes ($animationName) {
@-webkit-keyframes #{$animationName} { @-webkit-keyframes #{$animationName} {
@content; @content;
@@ -103,7 +95,6 @@ body {
width: 5px; width: 5px;
height: 5px; height: 5px;
border-radius: 50%; border-radius: 50%;
box-shadow: $box-shadow2;
@include animation((1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards)); @include animation((1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards));
} }
@@ -112,9 +103,9 @@ body {
@include animation-duration((1.25s, 1.25s, 6.25s)); @include animation-duration((1.25s, 1.25s, 6.25s));
} }
@include keyframes(bang) { @keyframes bang{
to { to{
box-shadow:$box-shadow; box-shadow:-314.6666666667px -362.6666666667px red,-51.6666666667px 32.3333333333px #ff3700,-354.6666666667px -264.6666666667px #7b00ff,-319.6666666667px -73.6666666667px #00f7ff,-135.6666666667px -154.6666666667px #00ff48,57.3333333333px -402.6666666667px #0d00ff,-126.6666666667px -121.6666666667px #00ff7b,-335.6666666667px -5.6666666667px #00fff2,-291.6666666667px -.6666666667px #4f0,-126.6666666667px -187.6666666667px #7f0,-413.6666666667px -224.6666666667px #00ffbf,-283.6666666667px -391.6666666667px #00ff3c,-340.6666666667px -345.6666666667px #02f,-168.6666666667px -179.6666666667px #eaff00,7.3333333333px -153.6666666667px #26ff00,-175.6666666667px -234.6666666667px #8400ff,-324.6666666667px -254.6666666667px #0048ff,-335.6666666667px -9.6666666667px #00ff59,-304.6666666667px -8.6666666667px #001eff,-331.6666666667px -44.6666666667px #3f0,.3333333333px -49.6666666667px #0fc,-370.6666666667px -60.6666666667px #0015ff,29.3333333333px -13.6666666667px #8cff00,-168.6666666667px -281.6666666667px #f80,-48.6666666667px -61.6666666667px #f0b,33.3333333333px -113.6666666667px #ff00e1,-193.6666666667px -196.6666666667px #ff7b00,-14.6666666667px -24.6666666667px #ff0037,-149.6666666667px -273.6666666667px #0fa,-19.6666666667px -63.6666666667px #ff0004,13.3333333333px -227.6666666667px #7f0,-265.6666666667px -43.6666666667px #ff4800,-121.6666666667px -95.6666666667px #bfff00,-241.6666666667px -90.6666666667px #6200ff,-307.6666666667px -231.6666666667px #ff0062,78.3333333333px -128.6666666667px #ffbf00,27.3333333333px 44.3333333333px #95ff00,-81.6666666667px 6.3333333333px #ffc800,-343.6666666667px -247.6666666667px #2f0,-225.6666666667px -250.6666666667px #08f,-9.6666666667px -243.6666666667px #ff1a00,83.3333333333px -409.6666666667px #04f,-380.6666666667px -331.6666666667px #84ff00,-103.6666666667px -51.6666666667px #f02,-174.6666666667px -169.6666666667px #ffc800,20.3333333333px -191.6666666667px #ff0059,-40.6666666667px -55.6666666667px #0400ff,-199.6666666667px -66.6666666667px #ffd500,-358.6666666667px -5.6666666667px #0051ff,-84.6666666667px -289.6666666667px #f7ff00,-193.6666666667px -184.6666666667px #80f
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -878,6 +878,10 @@
<context context-type="sourcefile">src/app/bisq/bisq-transactions/bisq-transactions.component.html</context> <context context-type="sourcefile">src/app/bisq/bisq-transactions/bisq-transactions.component.html</context>
<context context-type="linenumber">20,21</context> <context context-type="linenumber">20,21</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context> <context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
<context context-type="linenumber">124,125</context> <context context-type="linenumber">124,125</context>
@@ -1263,14 +1267,14 @@
<source>Trades</source> <source>Trades</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context> <context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="bisq-graph-volume" datatype="html"> <trans-unit id="bisq-graph-volume" datatype="html">
<source>Volume</source> <source>Volume</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context> <context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">100</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4b137ec8bf73a47063740b75c0c40d5fd3c48015" datatype="html"> <trans-unit id="4b137ec8bf73a47063740b75c0c40d5fd3c48015" datatype="html">
@@ -1918,15 +1922,6 @@
<context context-type="linenumber">264,266</context> <context context-type="linenumber">264,266</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="bc4c61d3713989e3c8c6610fca3ea1ca1cb19edb" datatype="html">
<source>Value</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<note priority="1" from="description">Transaction value</note>
<note priority="1" from="meaning">transaction.value</note>
</trans-unit>
<trans-unit id="cb1b52c13b95fa29ea4044f2bbe0ac623b890c80" datatype="html"> <trans-unit id="cb1b52c13b95fa29ea4044f2bbe0ac623b890c80" datatype="html">
<source>Fee</source> <source>Fee</source>
<context-group purpose="location"> <context-group purpose="location">

File diff suppressed because it is too large Load Diff

View File

@@ -1046,3 +1046,7 @@ th {
box-shadow: -10px -15px 75px rgba(#eba814, 1); box-shadow: -10px -15px 75px rgba(#eba814, 1);
transition: 100ms all ease-in; transition: 100ms all ease-in;
} }
.page-item {
font-family: monospace;
}

View File

@@ -82,11 +82,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
### Node.js + npm ### Node.js + npm
Build Node.js v16.15 and npm v8 from source using `nvm`: Build Node.js v16.16.0 and npm v8 from source using `nvm`:
``` ```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
source $HOME/.zshrc source $HOME/.zshrc
nvm install v16.15.0 nvm install v16.16.0 --shared-zlib
nvm alias default node nvm alias default node
``` ```

View File

@@ -183,6 +183,9 @@ case $OS in
TOR_PKG=tor TOR_PKG=tor
TOR_USER=_tor TOR_USER=_tor
TOR_GROUP=_tor TOR_GROUP=_tor
NGINX_USER=www
NGINX_ETC_FOLDER=/usr/local/etc/nginx
NGINX_CONFIGURATION=/usr/local/etc/nginx/nginx.conf
CERTBOT_PKG=py39-certbot CERTBOT_PKG=py39-certbot
;; ;;
@@ -197,6 +200,8 @@ case $OS in
TOR_USER=debian-tor TOR_USER=debian-tor
TOR_GROUP=debian-tor TOR_GROUP=debian-tor
CERTBOT_PKG=python3-certbot-nginx CERTBOT_PKG=python3-certbot-nginx
NGINX_USER=www-data
NGINX_ETC_FOLDER=/etc/nginx
NGINX_CONFIGURATION=/etc/nginx/nginx.conf NGINX_CONFIGURATION=/etc/nginx/nginx.conf
;; ;;
esac esac
@@ -646,193 +651,193 @@ ext4CreateDir()
# does bitcoin exist? # does bitcoin exist?
########## ###########
# dialog # ## dialog #
########## ###########
#
: ${DIALOG=dialog} #: ${DIALOG=dialog}
#
: ${DIALOG_OK=0} #: ${DIALOG_OK=0}
: ${DIALOG_CANCEL=1} #: ${DIALOG_CANCEL=1}
: ${DIALOG_HELP=2} #: ${DIALOG_HELP=2}
: ${DIALOG_EXTRA=3} #: ${DIALOG_EXTRA=3}
: ${DIALOG_ITEM_HELP=4} #: ${DIALOG_ITEM_HELP=4}
: ${DIALOG_ESC=255} #: ${DIALOG_ESC=255}
#
: ${SIG_OFFNE=0} #: ${SIG_OFFNE=0}
: ${SIG_HUP=1} #: ${SIG_HUP=1}
: ${SIG_INT=2} #: ${SIG_INT=2}
: ${SIG_QUIT=3} #: ${SIG_QUIT=3}
: ${SIG_KILL=9} #: ${SIG_KILL=9}
: ${SIG_TERM=15} #: ${SIG_TERM=15}
#
input=`tempfile 2>/dev/null` || input=/tmp/input$$ #input=`tempfile 2>/dev/null` || input=/tmp/input$$
output=`tempfile 2>/dev/null` || output=/tmp/test$$ #output=`tempfile 2>/dev/null` || output=/tmp/test$$
trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM #trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM
#
DIALOG_ERROR=254 #DIALOG_ERROR=254
export DIALOG_ERROR #export DIALOG_ERROR
#
backtitle="Mempool Fullnode Installer" #backtitle="Mempool Fullnode Installer"
title="Mempool Fullnode Installer" #title="Mempool Fullnode Installer"
returncode=0 #returncode=0
#
################# ##################
# dialog part 1 # ## dialog part 1 #
################# ##################
#
$CUT >$input <<-EOF #$CUT >$input <<-EOF
Tor:Enable Tor v3 HS Onion:ON #Tor:Enable Tor v3 HS Onion:ON
Certbot:Enable HTTPS using Certbot:ON #Certbot:Enable HTTPS using Certbot:ON
Mainnet:Enable Bitcoin Mainnet:ON #Mainnet:Enable Bitcoin Mainnet:ON
Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON #Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
Testnet:Enable Bitcoin Testnet:ON #Testnet:Enable Bitcoin Testnet:ON
Liquid:Enable Elements Liquid:ON #Liquid:Enable Elements Liquid:ON
Bisq:Enable Bisq:ON #Bisq:Enable Bisq:ON
Lightmode:Enable Electrs Lightmode to save disk space:ON #Lightmode:Enable Electrs Lightmode to save disk space:ON
Smalldisk:Disable Electrs Compaction to save disk space:ON #Smalldisk:Disable Electrs Compaction to save disk space:ON
Firewall:Enable Firewall:ON #Firewall:Enable Firewall:ON
EOF #EOF
#
cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output #cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output
cat $output >$input #cat $output >$input
#
$DIALOG --backtitle "${backtitle}" \ #$DIALOG --backtitle "${backtitle}" \
--title "${title}" "$@" \ # --title "${title}" "$@" \
--checklist "Toggle the features below to configure your fullnode:\n" \ # --checklist "Toggle the features below to configure your fullnode:\n" \
20 80 10 \ # 20 80 10 \
--file $input 2> $output # --file $input 2> $output
#
retval=$? #retval=$?
#
tempfile=$output #tempfile=$output
if [ $retval != $DIALOG_OK ];then #if [ $retval != $DIALOG_OK ];then
echo "Installation aborted." # echo "Installation aborted."
exit 1 # exit 1
fi #fi
#
if grep Tor $tempfile >/dev/null 2>&1;then #if grep Tor $tempfile >/dev/null 2>&1;then
TOR_INSTALL=ON # TOR_INSTALL=ON
else #else
TOR_INSTALL=OFF # TOR_INSTALL=OFF
fi #fi
#
if grep Certbot $tempfile >/dev/null 2>&1;then #if grep Certbot $tempfile >/dev/null 2>&1;then
CERTBOT_INSTALL=ON # CERTBOT_INSTALL=ON
else #else
CERTBOT_INSTALL=OFF # CERTBOT_INSTALL=OFF
fi #fi
#
if grep Mainnet $tempfile >/dev/null 2>&1;then #if grep Mainnet $tempfile >/dev/null 2>&1;then
BITCOIN_MAINNET_ENABLE=ON # BITCOIN_MAINNET_ENABLE=ON
else #else
BITCOIN_MAINNET_ENABLE=OFF # BITCOIN_MAINNET_ENABLE=OFF
fi #fi
#
if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then #if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then
BITCOIN_MAINNET_MINFEE_ENABLE=ON # BITCOIN_MAINNET_MINFEE_ENABLE=ON
else #else
BITCOIN_MAINNET_MINFEE_ENABLE=OFF # BITCOIN_MAINNET_MINFEE_ENABLE=OFF
fi #fi
#
if grep Testnet $tempfile >/dev/null 2>&1;then #if grep Testnet $tempfile >/dev/null 2>&1;then
BITCOIN_TESTNET_ENABLE=ON # BITCOIN_TESTNET_ENABLE=ON
else #else
BITCOIN_TESTNET_ENABLE=OFF # BITCOIN_TESTNET_ENABLE=OFF
fi #fi
#
if grep Liquid $tempfile >/dev/null 2>&1;then #if grep Liquid $tempfile >/dev/null 2>&1;then
ELEMENTS_INSTALL=ON # ELEMENTS_INSTALL=ON
ELEMENTS_LIQUID_ENABLE=ON # ELEMENTS_LIQUID_ENABLE=ON
else #else
ELEMENTS_INSTALL=OFF # ELEMENTS_INSTALL=OFF
ELEMENTS_LIQUID_ENABLE=OFF # ELEMENTS_LIQUID_ENABLE=OFF
fi #fi
#
if grep Bisq $tempfile >/dev/null 2>&1;then #if grep Bisq $tempfile >/dev/null 2>&1;then
BISQ_INSTALL=ON # BISQ_INSTALL=ON
BISQ_MAINNET_ENABLE=ON # BISQ_MAINNET_ENABLE=ON
else #else
BISQ_INSTALL=OFF # BISQ_INSTALL=OFF
BISQ_MAINNET_ENABLE=OFF # BISQ_MAINNET_ENABLE=OFF
fi #fi
#
if grep Lightmode $tempfile >/dev/null 2>&1;then #if grep Lightmode $tempfile >/dev/null 2>&1;then
BITCOIN_ELECTRS_LIGHT_MODE=ON # BITCOIN_ELECTRS_LIGHT_MODE=ON
else #else
BITCOIN_ELECTRS_LIGHT_MODE=OFF # BITCOIN_ELECTRS_LIGHT_MODE=OFF
fi #fi
#
if grep Smalldisk $tempfile >/dev/null 2>&1;then #if grep Smalldisk $tempfile >/dev/null 2>&1;then
BITCOIN_ELECTRS_LIGHT_MODE=ON # BITCOIN_ELECTRS_LIGHT_MODE=ON
else #else
BITCOIN_ELECTRS_LIGHT_MODE=OFF # BITCOIN_ELECTRS_LIGHT_MODE=OFF
fi #fi
#
################# ##################
# dialog part 2 # ## dialog part 2 #
################# ##################
#
$DIALOG --cr-wrap \ #$DIALOG --cr-wrap \
--title "INPUT BOX" --clear \ # --title "INPUT BOX" --clear \
--inputbox "$@" \ # --inputbox "$@" \
"Enter the FQDN hostname for obtaining an SSL certificate using Certbot:" 0 0 "${HOSTNAME}" 2> $tempfile #"Enter the FQDN hostname for obtaining an SSL certificate using Certbot:" 0 0 "${HOSTNAME}" 2> $tempfile
HOSTNAME=$(cat $tempfile) #HOSTNAME=$(cat $tempfile)
#
################# ##################
# dialog part 3 # ## dialog part 3 #
################# ##################
#
# --form text height width formheight ## --form text height width formheight
# [ label y x item y x flen ilen ] ## [ label y x item y x flen ilen ]
#"BISQ_BLOCKNOTIFY_HOST" 0 1 "${BISQ_BLOCKNOTIFY_HOST}" 0 30 0 0 \ # #"BISQ_BLOCKNOTIFY_HOST" 0 1 "${BISQ_BLOCKNOTIFY_HOST}" 0 30 0 0 \
#
$DIALOG --ok-label "Submit" \ #$DIALOG --ok-label "Submit" \
--backtitle "$backtitle" "$@" \ # --backtitle "$backtitle" "$@" \
--form "Your fullnode will be installed as follows:" 0 0 0 \ # --form "Your fullnode will be installed as follows:" 0 0 0 \
"BISQ_LATEST_RELEASE" 1 1 "${BISQ_LATEST_RELEASE}" 1 35 35 0 \ # "BISQ_LATEST_RELEASE" 1 1 "${BISQ_LATEST_RELEASE}" 1 35 35 0 \
"BISQ_REPO_BRANCH" 2 1 "${BISQ_REPO_BRANCH}" 2 35 35 0 \ # "BISQ_REPO_BRANCH" 2 1 "${BISQ_REPO_BRANCH}" 2 35 35 0 \
"BISQ_REPO_NAME" 3 1 "${BISQ_REPO_NAME}" 3 35 35 0 \ # "BISQ_REPO_NAME" 3 1 "${BISQ_REPO_NAME}" 3 35 35 0 \
"BISQ_REPO_URL" 4 1 "${BISQ_REPO_URL}" 4 35 35 0 \ # "BISQ_REPO_URL" 4 1 "${BISQ_REPO_URL}" 4 35 35 0 \
"BITCOIN_ELECTRS_LATEST_RELEASE" 5 1 "${BITCOIN_ELECTRS_LATEST_RELEASE}" 5 35 35 0 \ # "BITCOIN_ELECTRS_LATEST_RELEASE" 5 1 "${BITCOIN_ELECTRS_LATEST_RELEASE}" 5 35 35 0 \
"BITCOIN_ELECTRS_LIGHT_MODE" 6 1 "${BITCOIN_ELECTRS_LIGHT_MODE}" 6 35 35 0 \ # "BITCOIN_ELECTRS_LIGHT_MODE" 6 1 "${BITCOIN_ELECTRS_LIGHT_MODE}" 6 35 35 0 \
"BITCOIN_ELECTRS_REPO_BRANCH" 7 1 "${BITCOIN_ELECTRS_REPO_BRANCH}" 7 35 35 0 \ # "BITCOIN_ELECTRS_REPO_BRANCH" 7 1 "${BITCOIN_ELECTRS_REPO_BRANCH}" 7 35 35 0 \
"BITCOIN_ELECTRS_REPO_NAME" 8 1 "${BITCOIN_ELECTRS_REPO_NAME}" 8 35 35 0 \ # "BITCOIN_ELECTRS_REPO_NAME" 8 1 "${BITCOIN_ELECTRS_REPO_NAME}" 8 35 35 0 \
"BITCOIN_ELECTRS_REPO_URL" 9 1 "${BITCOIN_ELECTRS_REPO_URL}" 9 35 35 0 \ # "BITCOIN_ELECTRS_REPO_URL" 9 1 "${BITCOIN_ELECTRS_REPO_URL}" 9 35 35 0 \
"BITCOIN_LATEST_RELEASE" 10 1 "${BITCOIN_LATEST_RELEASE}" 10 35 35 0 \ # "BITCOIN_LATEST_RELEASE" 10 1 "${BITCOIN_LATEST_RELEASE}" 10 35 35 0 \
"BITCOIN_MAINNET_ENABLE" 11 1 "${BITCOIN_MAINNET_ENABLE}" 11 35 35 0 \ # "BITCOIN_MAINNET_ENABLE" 11 1 "${BITCOIN_MAINNET_ENABLE}" 11 35 35 0 \
"BITCOIN_REPO_BRANCH" 12 1 "${BITCOIN_REPO_BRANCH}" 12 35 35 0 \ # "BITCOIN_REPO_BRANCH" 12 1 "${BITCOIN_REPO_BRANCH}" 12 35 35 0 \
"BITCOIN_REPO_NAME" 13 1 "${BITCOIN_REPO_NAME}" 13 35 35 0 \ # "BITCOIN_REPO_NAME" 13 1 "${BITCOIN_REPO_NAME}" 13 35 35 0 \
"BITCOIN_REPO_URL" 14 1 "${BITCOIN_REPO_URL}" 14 35 35 0 \ # "BITCOIN_REPO_URL" 14 1 "${BITCOIN_REPO_URL}" 14 35 35 0 \
"BITCOIN_TESTNET_ENABLE" 15 1 "${BITCOIN_TESTNET_ENABLE}" 15 35 35 0 \ # "BITCOIN_TESTNET_ENABLE" 15 1 "${BITCOIN_TESTNET_ENABLE}" 15 35 35 0 \
"ELEMENTS_INSTALL" 16 1 "${ELEMENTS_INSTALL}" 16 35 35 0 \ # "ELEMENTS_INSTALL" 16 1 "${ELEMENTS_INSTALL}" 16 35 35 0 \
"ELEMENTS_LATEST_RELEASE" 17 1 "${ELEMENTS_LATEST_RELEASE}" 17 35 35 0 \ # "ELEMENTS_LATEST_RELEASE" 17 1 "${ELEMENTS_LATEST_RELEASE}" 17 35 35 0 \
"ELEMENTS_LIQUID_ENABLE" 18 1 "${ELEMENTS_LIQUID_ENABLE}" 18 35 35 0 \ # "ELEMENTS_LIQUID_ENABLE" 18 1 "${ELEMENTS_LIQUID_ENABLE}" 18 35 35 0 \
"ELEMENTS_REPO_BRANCH" 19 1 "${ELEMENTS_REPO_BRANCH}" 19 35 35 0 \ # "ELEMENTS_REPO_BRANCH" 19 1 "${ELEMENTS_REPO_BRANCH}" 19 35 35 0 \
"ELEMENTS_REPO_NAME" 20 1 "${ELEMENTS_REPO_NAME}" 20 35 35 0 \ # "ELEMENTS_REPO_NAME" 20 1 "${ELEMENTS_REPO_NAME}" 20 35 35 0 \
"ELEMENTS_REPO_URL" 21 1 "${ELEMENTS_REPO_URL}" 21 35 35 0 \ # "ELEMENTS_REPO_URL" 21 1 "${ELEMENTS_REPO_URL}" 21 35 35 0 \
"MEMPOOL_LATEST_RELEASE" 22 1 "${MEMPOOL_LATEST_RELEASE}" 22 35 35 0 \ # "MEMPOOL_LATEST_RELEASE" 22 1 "${MEMPOOL_LATEST_RELEASE}" 22 35 35 0 \
"MEMPOOL_LIQUID_HTTP_HOST" 23 1 "${MEMPOOL_LIQUID_HTTP_HOST}" 23 35 35 0 \ # "MEMPOOL_LIQUID_HTTP_HOST" 23 1 "${MEMPOOL_LIQUID_HTTP_HOST}" 23 35 35 0 \
"MEMPOOL_LIQUID_HTTP_PORT" 24 1 "${MEMPOOL_LIQUID_HTTP_PORT}" 24 35 35 0 \ # "MEMPOOL_LIQUID_HTTP_PORT" 24 1 "${MEMPOOL_LIQUID_HTTP_PORT}" 24 35 35 0 \
"MEMPOOL_MAINNET_HTTP_HOST" 25 1 "${MEMPOOL_MAINNET_HTTP_HOST}" 25 35 35 0 \ # "MEMPOOL_MAINNET_HTTP_HOST" 25 1 "${MEMPOOL_MAINNET_HTTP_HOST}" 25 35 35 0 \
"MEMPOOL_MAINNET_HTTP_PORT" 26 1 "${MEMPOOL_MAINNET_HTTP_PORT}" 26 35 35 0 \ # "MEMPOOL_MAINNET_HTTP_PORT" 26 1 "${MEMPOOL_MAINNET_HTTP_PORT}" 26 35 35 0 \
"MEMPOOL_REPO_BRANCH" 27 1 "${MEMPOOL_REPO_BRANCH}" 27 35 35 0 \ # "MEMPOOL_REPO_BRANCH" 27 1 "${MEMPOOL_REPO_BRANCH}" 27 35 35 0 \
"MEMPOOL_REPO_NAME" 28 1 "${MEMPOOL_REPO_NAME}" 28 35 35 0 \ # "MEMPOOL_REPO_NAME" 28 1 "${MEMPOOL_REPO_NAME}" 28 35 35 0 \
"MEMPOOL_REPO_URL" 29 1 "${MEMPOOL_REPO_URL}" 29 35 35 0 \ # "MEMPOOL_REPO_URL" 29 1 "${MEMPOOL_REPO_URL}" 29 35 35 0 \
"MEMPOOL_TESTNET_HTTP_HOST" 30 1 "${MEMPOOL_TESTNET_HTTP_HOST}" 30 35 35 0 \ # "MEMPOOL_TESTNET_HTTP_HOST" 30 1 "${MEMPOOL_TESTNET_HTTP_HOST}" 30 35 35 0 \
"MEMPOOL_TESTNET_HTTP_PORT" 31 1 "${MEMPOOL_TESTNET_HTTP_PORT}" 31 35 35 0 \ # "MEMPOOL_TESTNET_HTTP_PORT" 31 1 "${MEMPOOL_TESTNET_HTTP_PORT}" 31 35 35 0 \
"MEMPOOL_TOR_HS" 32 1 "${MEMPOOL_TOR_HS}" 32 35 35 0 \ # "MEMPOOL_TOR_HS" 32 1 "${MEMPOOL_TOR_HS}" 32 35 35 0 \
"HOSTNAME" 33 1 "${HOSTNAME}" 33 35 35 0 \ # "HOSTNAME" 33 1 "${HOSTNAME}" 33 35 35 0 \
"TOR_INSTALL" 34 1 "${TOR_INSTALL}" 34 35 35 0 \ # "TOR_INSTALL" 34 1 "${TOR_INSTALL}" 34 35 35 0 \
"CERTBOT_INSTALL" 35 1 "${CERTBOT_INSTALL}" 35 35 35 0 \ # "CERTBOT_INSTALL" 35 1 "${CERTBOT_INSTALL}" 35 35 35 0 \
2> $tempfile #2> $tempfile
#
retval=$? #retval=$?
#
if [ $retval != $DIALOG_OK ];then #if [ $retval != $DIALOG_OK ];then
echo "Installation aborted." # echo "Installation aborted."
exit 1 # exit 1
fi #fi
############################ ############################
# START DOING ACTUAL STUFF # # START DOING ACTUAL STUFF #
@@ -841,8 +846,6 @@ fi
date date
echo "[*] Mempool installation script for ${OS}" echo "[*] Mempool installation script for ${OS}"
set -x
################################### ###################################
# create filesystems if necessary # # create filesystems if necessary #
################################### ###################################
@@ -906,7 +909,7 @@ echo "[*] Installing nvm.sh from GitHub"
osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh' osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
echo "[*] Building NodeJS via nvm.sh" echo "[*] Building NodeJS via nvm.sh"
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.15.0' osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.16.0 --shared-zlib'
#################### ####################
# Tor installation # # Tor installation #
@@ -925,6 +928,8 @@ if [ "${TOR_INSTALL}" = ON ];then
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}" osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}"
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}" osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}"
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}" osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
else
osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}"
fi fi
case $OS in case $OS in
@@ -936,7 +941,7 @@ if [ "${TOR_INSTALL}" = ON ];then
# start tor now so it can bootstrap in time for bitcoin starting a few mins later # start tor now so it can bootstrap in time for bitcoin starting a few mins later
echo "[*] Starting Tor service" echo "[*] Starting Tor service"
osSudo "${ROOT_USER}" service tor start osSudo "${ROOT_USER}" service tor restart
fi fi
######################## ########################
@@ -1282,7 +1287,25 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
;; ;;
Debian) Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/liquid.service" "${DEBIAN_SERVICE_HOME}" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/elements-liquid.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
#######################################
# Bitcoin instance for Liquid Testnet #
#######################################
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Liquid service"
case $OS in
FreeBSD)
echo "[*] FIXME: Bitcoin Liquid service must be installed manually on FreeBSD"
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/elements-liquidtestnet.service" "${DEBIAN_SERVICE_HOME}"
;; ;;
esac esac
fi fi
@@ -1295,18 +1318,6 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Mainnet electrs start script" echo "[*] Installing Bitcoin Mainnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
echo "[*] Installing Bitcoin crontab"
case $OS in
FreeBSD)
echo [*] FIXME: must only crontab enabled daemons
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
;;
Debian)
(crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
@@ -1321,13 +1332,6 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Testnet electrs start script" echo "[*] Installing Bitcoin Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Bitcoin-testnet crontab"
(crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
@@ -1342,13 +1346,6 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Signet electrs start script" echo "[*] Installing Bitcoin Signet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Bitcoin-signet crontab"
(crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
@@ -1366,12 +1363,9 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
echo "[*] Installing Elements crontab" echo "[*] Installing Elements crontab"
case $OS in case $OS in
FreeBSD) FreeBSD)
echo [*] FIXME: must only crontab enabled daemons echo "[*] FIXME: must only crontab enabled daemons"
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab" osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
;; ;;
Debian)
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
;;
esac esac
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script" echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
@@ -1388,13 +1382,6 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
echo "[*] Installing Elements Liquid Testnet electrs start script" echo "[*] Installing Elements Liquid Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Elements-testnet crontab"
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
;;
esac
echo "[*] Installing Elements Liquid Testnet RPC credentials" echo "[*] Installing Elements Liquid Testnet RPC credentials"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
@@ -1407,6 +1394,45 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
fi fi
################################
# Install all Electrs Cronjobs #
################################
echo "[*] Installing crontabs"
case $OS in
FreeBSD)
echo "[*] FIXME: must only crontab enabled daemons"
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
;;
Debian)
crontab_bitcoin=()
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
echo "[*] Installing Electrs Mainnet Cronjob"
crontab_bitcoin+="@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet\n"
fi
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Electrs Testnet Cronjob"
crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet\n"
fi
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Electrs Signet Cronjob"
crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n"
fi
echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" -
crontab_elements=()
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
echo "[*] Installing Liquid Asset Mainnet Cronjob"
crontab_elements+="6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1\n"
fi
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
echo "[*] Installing Liquid Asset Testnet Cronjob"
crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n"
fi
echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" -
;;
esac
##################################### #####################################
# Bisq instance for Bitcoin Mainnet # # Bisq instance for Bitcoin Mainnet #
##################################### #####################################
@@ -1505,19 +1531,24 @@ _EOF_
##### nginx ##### nginx
echo "[*] Read tor v3 onion hostnames"
NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname")
NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname")
NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname")
echo "[*] Adding Nginx configuration" echo "[*] Adding Nginx configuration"
case $OS in osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
mkdir -p /var/cache/nginx/services /var/cache/nginx/api
FreeBSD) chown ${NGINX_USER}: /var/cache/nginx/services /var/cache/nginx/api
echo "[*] FIXME: nginx must be configured manually on FreeBSD" ln -s /mempool/mempool /etc/nginx/mempool
;; osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_USER__!${NGINX_USER}!" "${NGINX_CONFIGURATION}"
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_ETC_FOLDER__!${NGINX_ETC_FOLDER}!" "${NGINX_CONFIGURATION}"
Debian) osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}"
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}" osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}"
#echo "[*] Restarting Nginx" osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}"
#osSudo "${ROOT_USER}" service nginx restart echo "[*] Restarting Nginx"
;; osSudo "${ROOT_USER}" service nginx restart
esac
##### OS systemd ##### OS systemd
@@ -1548,11 +1579,40 @@ case $OS in
osSudo "${ROOT_USER}" systemctl enable bisq.service osSudo "${ROOT_USER}" systemctl enable bisq.service
fi fi
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable liquid.service osSudo "${ROOT_USER}" systemctl enable elements-liquid.service
fi
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable elements-liquidtestnet.service
fi fi
;; ;;
esac esac
##### OS set Linux user ulimits
echo "[*] Setting ulimits for users"
case $OS in
FreeBSD)
;;
Debian)
cat >> /etc/security/limits.conf <<EOF
* soft nproc 200000
* hard nproc 200000
* soft nofile 200000
* hard nofile 200000
EOF
echo "session required pam_limits.so" >> /etc/pam.d/common-session
;;
esac
##### Build Mempool
echo "[*] Build Mempool"
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME} && ./upgrade"
##### OS services ##### OS services
#if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then #if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
@@ -1628,6 +1688,21 @@ esac
##### finish ##### finish
case $OS in
FreeBSD)
;;
Debian)
echo "This are the generated Tor addresses:"
echo "${NGINX_MEMPOOL_ONION}"
echo "${NGINX_BISQ_ONION}"
echo "${NGINX_LIQUID_ONION}"
;;
esac
echo
echo 'Please reboot to start all the services.'
echo '[*] Done!' echo '[*] Done!'
exit 0 exit 0

View File

@@ -1,13 +1,13 @@
[Unit] [Unit]
Description=Elementsd Description=Elementsd-liquid
After=network.target After=network.target
[Service] [Service]
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidtestnet -pid=/elements/elements-testnet.pid ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidv1 -pid=/elements/elements-liquid.pid
ExecStop=/usr/local/bin/elements-cli stop ExecStop=/usr/local/bin/elements-cli stop
Type=forking Type=forking
PIDFile=/elements/elements-testnet.pid PIDFile=/elements/elements-liquid.pid
Restart=on-failure Restart=on-failure
User=elements User=elements

View File

@@ -1,13 +1,13 @@
[Unit] [Unit]
Description=Elementsd Description=Elementsd-liquidtestnet
After=network.target After=network.target
[Service] [Service]
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidv1 -pid=/elements/elements.pid ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidtestnet -pid=/elements/elements-liquidtestnet.pid
ExecStop=/usr/local/bin/elements-cli stop ExecStop=/usr/local/bin/elements-cli stop
Type=forking Type=forking
PIDFile=/elements/elements.pid PIDFile=/elements/elements-liquidtestnet.pid
Restart=on-failure Restart=on-failure
User=elements User=elements

View File

@@ -56,7 +56,7 @@ build_frontend()
if [ ! -e "mempool-frontend-config.json" ];then if [ ! -e "mempool-frontend-config.json" ];then
cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json" cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json"
fi fi
npm install --no-optional || exit 1 npm install --omit=dev --omit=optional || exit 1
npm run build || exit 1 npm run build || exit 1
} }
@@ -75,7 +75,7 @@ build_backend()
-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \ -e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
"mempool-config.json" "mempool-config.json"
fi fi
npm install --no-optional || exit 1 npm install --omit=dev --omit=optional || exit 1
npm run build || exit 1 npm run build || exit 1
} }

View File

@@ -1,4 +1,4 @@
user nobody; user __NGINX_USER__;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
worker_processes auto; worker_processes auto;
@@ -10,11 +10,11 @@ events {
} }
http { http {
# DNS servers for on-demand recursive resolver # DNS servers for on-demand resolution, change if desired
resolver 8.8.8.8; resolver 8.8.8.8;
# include default mime types # include default mime types
include /usr/local/etc/nginx/mime.types; include __NGINX_ETC_FOLDER__/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
# HTTP basic configuration # HTTP basic configuration
@@ -32,9 +32,13 @@ http {
# MEMPOOL.NINJA # MEMPOOL.NINJA
server { server {
# clearnet v4/v6 # clearnet v4/v6
listen 443 ssl http2; #listen 443 ssl http2;
listen [::]:443 ssl http2; #listen [::]:443 ssl http2;
server_name mempool.ninja; server_name _;
# tor v3
listen 127.0.0.1:81;
set $onion "__NGINX_MEMPOOL_ONION__";
# for services from mempool.space like contributors on about page # for services from mempool.space like contributors on about page
set $mempoolSpaceServices "https://mempool.space"; set $mempoolSpaceServices "https://mempool.space";
@@ -52,30 +56,24 @@ http {
set $esploraTestnet "http://esplora-bitcoin-testnet"; set $esploraTestnet "http://esplora-bitcoin-testnet";
set $esploraSignet "http://esplora-bitcoin-signet"; set $esploraSignet "http://esplora-bitcoin-signet";
# tor v3
listen 127.0.0.1:81;
set $onion "mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad";
# filesystem paths # filesystem paths
root /mempool/public_html/mainnet/; root /mempool/public_html/mainnet/;
access_log /var/log/nginx/mempool-access.log; access_log /var/log/nginx/mempool-access.log;
error_log /var/log/nginx/mempool-error.log; error_log /var/log/nginx/mempool-error.log;
# ssl configuration
ssl_certificate /usr/local/etc/letsencrypt/live/mempool.ninja/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/mempool.ninja/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
# site configuration # site configuration
include mempool/production/nginx/server-mempool.conf; include mempool/production/nginx/server-mempool.conf;
} }
# BISQ.NINJA # BISQ.NINJA
server { server {
# clearnet v4/v6 # clearnet v4/v6
listen 443 ssl http2; #listen 443 ssl http2;
listen [::]:443 ssl http2; #listen [::]:443 ssl http2;
server_name bisq.ninja; server_name _;
# tor v3
listen 127.0.0.1:82;
set $onion "__NGINX_BISQ_ONION__";
# for services from mempool.space like contributors on about page # for services from mempool.space like contributors on about page
set $mempoolSpaceServices "https://mempool.space"; set $mempoolSpaceServices "https://mempool.space";
@@ -86,30 +84,24 @@ http {
# for blockstream/esplora daemon, see upstream-esplora.conf # for blockstream/esplora daemon, see upstream-esplora.conf
set $esploraMainnet "http://esplora-bitcoin-mainnet"; set $esploraMainnet "http://esplora-bitcoin-mainnet";
# tor v3
listen 127.0.0.1:82;
set $onion "bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd";
# filesystem paths # filesystem paths
root /mempool/public_html/bisq/; root /mempool/public_html/bisq/;
access_log /var/log/nginx/bisq-access.log; access_log /var/log/nginx/bisq-access.log;
error_log /var/log/nginx/bisq-error.log; error_log /var/log/nginx/bisq-error.log;
# ssl configuration
ssl_certificate /usr/local/etc/letsencrypt/live/bisq.ninja/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/bisq.ninja/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
# site configuration # site configuration
include mempool/production/nginx/server-bisq.conf; include mempool/production/nginx/server-bisq.conf;
} }
# LIQUID.PLACE # LIQUID.PLACE
server { server {
# clearnet v4/v6 # clearnet v4/v6
listen 443 ssl http2; #listen 443 ssl http2;
listen [::]:443 ssl http2; #listen [::]:443 ssl http2;
server_name liquid.place; server_name _;
# tor v3
listen 127.0.0.1:83;
set $onion "__NGINX_LIQUID_ONION__";
# for services from mempool.space like contributors on about page # for services from mempool.space like contributors on about page
set $mempoolSpaceServices "https://mempool.space"; set $mempoolSpaceServices "https://mempool.space";
@@ -122,36 +114,12 @@ http {
set $esploraMainnet "http://esplora-liquid-mainnet"; set $esploraMainnet "http://esplora-liquid-mainnet";
set $esploraTestnet "http://esplora-liquid-testnet"; set $esploraTestnet "http://esplora-liquid-testnet";
# tor v3
listen 127.0.0.1:83;
set $onion "liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd";
# filesystem paths # filesystem paths
root /mempool/public_html/liquid/; root /mempool/public_html/liquid/;
access_log /var/log/nginx/liquid-access.log; access_log /var/log/nginx/liquid-access.log;
error_log /var/log/nginx/liquid-error.log; error_log /var/log/nginx/liquid-error.log;
# ssl configuration
ssl_certificate /usr/local/etc/letsencrypt/live/liquid.place/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/liquid.place/privkey.pem;
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
# site configuration # site configuration
include mempool/production/nginx/server-liquid.conf; include mempool/production/nginx/server-liquid.conf;
} }
# HTTP to HTTPS redirect
server {
# clearnet v4/v6
listen 80;
listen [::]:80;
server_name _;
# only redirect for our hosted domains
if ($host ~ "^(mempool.ninja|bisq.ninja|liquid.place)$")
{
return 301 https://$host$request_uri;
}
return 503;
}
} }

View File

@@ -3,21 +3,21 @@ SOCKSPort 9050
ControlPort 9051 ControlPort 9051
Log notice syslog Log notice syslog
DataDirectory /var/db/tor DataDirectory __TOR_RESOURCES__
DataDirectoryGroupReadable 1 DataDirectoryGroupReadable 1
CookieAuthentication 1 CookieAuthentication 1
CookieAuthFile /var/db/tor/control_auth_cookie CookieAuthFile __TOR_RESOURCES__/control_auth_cookie
CookieAuthFileGroupReadable 1 CookieAuthFileGroupReadable 1
HiddenServiceDir /var/db/tor/mempool HiddenServiceDir __TOR_RESOURCES__/mempool
HiddenServicePort 80 127.0.0.1:81 HiddenServicePort 80 127.0.0.1:81
HiddenServiceVersion 3 HiddenServiceVersion 3
HiddenServiceDir /var/db/tor/bisq HiddenServiceDir __TOR_RESOURCES__/bisq
HiddenServicePort 80 127.0.0.1:82 HiddenServicePort 80 127.0.0.1:82
HiddenServiceVersion 3 HiddenServiceVersion 3
HiddenServiceDir /var/db/tor/liquid HiddenServiceDir __TOR_RESOURCES__/liquid
HiddenServicePort 80 127.0.0.1:83 HiddenServicePort 80 127.0.0.1:83
HiddenServiceVersion 3 HiddenServiceVersion 3