Compare commits

...

134 Commits

Author SHA1 Message Date
wiz
936964d273 Merge pull request #1839 from mempool/wiz/fix-docker-backend-start-script
Add missing sed substitutions in docker/backend/start.sh
2022-06-08 08:05:41 +09:00
wiz
4d274a3cec Add missing sed substitutions in docker/backend/start.sh
Fixes #1838
2022-06-08 07:50:21 +09:00
wiz
acd342259f Merge pull request #1835 from mempool/simon/transifex-pull-0608
Pulled from Transifex
2022-06-08 05:46:16 +09:00
softsimon
67456c151f Pulled from Transifex 2022-06-08 00:44:04 +04:00
wiz
13ccf55cc8 Merge pull request #1830 from hunicus/faq-projected-mempool
Change "projected block" to "mempool block" in feerate faq
2022-06-08 05:38:48 +09:00
wiz
73bffb5552 Merge pull request #1834 from mempool/nymkappa/bugfix/pools-import
Fix pool import crash
2022-06-08 05:38:38 +09:00
nymkappa
be8ee52af0 Fix pool import crash 2022-06-07 22:18:51 +02:00
hunicus
fbb16d6f22 Change projected to mempool in feerate faq 2022-06-07 14:54:33 -04:00
softsimon
96f8bf4a34 Merge pull request #1829 from mempool/simon/transifex-extract-0607
Extracting i18n
2022-06-07 22:12:44 +04:00
softsimon
2f9a86524a Extracting i18n 2022-06-07 22:11:48 +04:00
softsimon
e617e09ae3 Merge pull request #1827 from hunicus/change-terms
Change 'projected blocks' to 'mempool blocks' in feerate tooltips
2022-06-07 22:09:05 +04:00
wiz
6934aef60b Merge pull request #1828 from mempool/wiz/fix-upgrade-script-exit-code
[ops] Fix upgrade script exit code
2022-06-08 03:08:42 +09:00
hunicus
8f4de39e7b Change 'projected' to 'mempool' in feerate tooltips 2022-06-07 14:06:56 -04:00
wiz
fcb0c51e51 [ops] Fix mempool-build-all script exit code 2022-06-08 03:06:54 +09:00
wiz
ec80eac6b9 Merge pull request #1820 from hunicus/update-feerate-tooltips
Add feerate tooltips
2022-06-08 02:41:02 +09:00
wiz
84e600ac9f Merge branch 'master' into update-feerate-tooltips 2022-06-08 02:26:29 +09:00
wiz
c64d95b0ec Merge pull request #1822 from mempool/nymkappa/bugfix/db-disabled
Add 'db-less' mining pool tagging support
2022-06-08 02:26:02 +09:00
hunicus
3e2ced2e8b Make feerate tooltips appear on top 2022-06-07 13:18:20 -04:00
hunicus
6cc04feda8 Move feerate tooltips to feerate labels 2022-06-07 13:14:30 -04:00
wiz
0b50c17ed0 Merge branch 'master' into nymkappa/bugfix/db-disabled 2022-06-08 01:56:52 +09:00
wiz
81b9153d2b Merge branch 'master' into update-feerate-tooltips 2022-06-08 01:12:38 +09:00
wiz
e7c5307ca4 Merge pull request #1819 from hunicus/update-fee-faq-2
Update fee faq
2022-06-08 01:12:30 +09:00
wiz
8fb377b4eb Merge branch 'master' into update-fee-faq-2 2022-06-08 00:48:43 +09:00
wiz
5642358937 Merge pull request #1824 from hunicus/add-tx-note-1
Add transifex note to issue template
2022-06-08 00:47:39 +09:00
wiz
00cd1386b5 Merge pull request #1825 from hunicus/add-tx-note-2
Add pull request template to discourage tx prs
2022-06-08 00:47:15 +09:00
wiz
da6c72e9b7 Merge pull request #1691 from ayanamitech/cache-static
Handle Error with basic retry while syncing external assets ( Price Data )
2022-06-08 00:46:10 +09:00
hunicus
c318993a79 Add transifex note to issue template 2022-06-07 09:09:38 -04:00
hunicus
87c6e957f0 Remove unnecessary detail in fee faq 2022-06-07 08:56:41 -04:00
hunicus
e133467ea1 Add pull request template to discourage tx prs 2022-06-07 08:41:07 -04:00
hunicus
a0429b243f Make feerate tooltip i18n identifiers unique 2022-06-07 08:32:58 -04:00
nymkappa
21ae1fce2a Fix js crash when sending invalid state to /block page 2022-06-07 12:19:36 +02:00
nymkappa
53bc80e899 Add 'db-less' mining pool tagging support 2022-06-07 11:28:39 +02:00
Ayanami
56dc337672 Temporary disable retries
Until we find out how to sync async
2022-06-07 04:16:37 +09:00
Ayanami
a04bafdb4c Correct the log if the onion address is enabled or not 2022-06-07 04:16:37 +09:00
Ayanami
6ff473ab5d Add an ability to change circuits 2022-06-07 04:16:37 +09:00
Ayanami
40bfc6bff3 Include SocksProxyAgent inside while loop
To address error Socks5 proxy rejected connection - Failure
2022-06-07 04:16:37 +09:00
Ayanami
c610cacee4 Added missing config value
addressing comments from @knorrium
2022-06-07 04:16:36 +09:00
Ayanami
e41a08789a Added configurable user-agent for axios
Will use `mempool/v${backendInfo.getBackendInfo().version}` for default
2022-06-07 04:16:36 +09:00
Ayanami
9d5bbf1f44 Handle Error with basic retry while syncing external assets ( Price Data )
+ Removed unused External Assets value

+ Make static URL dynamic

+ Added config options for syncing pool data

+ Added retry interval & max retry
2022-06-07 04:16:36 +09:00
hunicus
22268b8a33 Add tooltips describing feerate levels 2022-06-06 14:32:16 -04:00
hunicus
0f58ce2322 Show fiat fee tooltip on fiat fee only 2022-06-06 14:17:25 -04:00
hunicus
1aad89ac97 Update fee faq 2022-06-06 13:05:53 -04:00
wiz
e99a684354 Merge pull request #1811 from mempool/simon/reload-mempool-visualizer-on-reconnect
Reload mempool visualization on reconnect
2022-06-06 21:35:05 +09:00
wiz
5360f6dd77 Merge branch 'master' into simon/reload-mempool-visualizer-on-reconnect 2022-06-06 21:23:48 +09:00
wiz
c8d5708155 Merge pull request #1812 from mempool/wiz/fix-foundry-logo-about-page
Use Foundry's dark theme logo on About page
2022-06-06 21:02:06 +09:00
softsimon
ebda00dc74 Send empty list of transactions if data isn't available yet 2022-06-06 14:31:17 +04:00
wiz
789092c76a Update Foundry logo on About page 2022-06-06 06:35:09 +09:00
softsimon
967a2a4461 Reload mempool visualization on reconnect
fixes #1799
2022-06-05 23:40:36 +04:00
wiz
9288628ad7 Merge pull request #1810 from mempool/simon/transifex-pull-0605
Transifex pull
2022-06-06 04:20:12 +09:00
wiz
0384ebb2ff Merge pull request #1745 from antonilol/multisig-label
stricter multisig check + detect bare multisig when spent
2022-06-06 04:19:57 +09:00
wiz
869c40e835 Merge branch 'master' into multisig-label 2022-06-06 04:06:43 +09:00
wiz
579af85544 Merge pull request #1806 from knorrium/cypress10_upgrade
Cypress v10 upgrade
2022-06-06 04:06:24 +09:00
wiz
97f72c1faf Merge branch 'master' into cypress10_upgrade 2022-06-06 03:53:09 +09:00
wiz
262c3af33e Merge pull request #1808 from mempool/nymkappa/bugfix/fix-blocks-list-loading
Make sure blocks list rxjs observable triggers at least once
2022-06-06 03:43:59 +09:00
softsimon
dd7d9b66e5 Transifex pull 2022-06-05 22:39:36 +04:00
wiz
f688da957c Merge branch 'master' into nymkappa/bugfix/fix-blocks-list-loading 2022-06-06 03:37:39 +09:00
wiz
866ac3d5b8 Merge pull request #1809 from mempool/simon/mempool-logo-svg-bugfix
Fix for mempool logo SVG issue
2022-06-06 03:36:41 +09:00
softsimon
63fce2a3ca Fix for mempool logo SVG issue 2022-06-05 22:29:05 +04:00
nymkappa
33e0859847 Make sure blocks list rxjs observable triggers at least once 2022-06-05 17:33:23 +02:00
Felipe Knorr Kuhn
b71922fabf Update Cypress Github Action to v4 2022-06-03 20:59:31 -07:00
Felipe Knorr Kuhn
ce0564a89c Update Cypress Github Action to v4 2022-06-03 20:59:03 -07:00
Felipe Knorr Kuhn
2a287b8d66 Update Cypress triggers back to push and pull request 2022-06-03 20:52:59 -07:00
Felipe Knorr Kuhn
69713ae156 Update to Cypress v10 2022-06-03 20:46:33 -07:00
Antoni Spaanderman
b930b9bf4f stricter multisig check + detect bare multisig when spent 2022-06-03 18:47:44 +02:00
wiz
412f118d22 Merge pull request #1801 from mempool/nymkappa/feature/cache-log
Improve disk cache logging
2022-06-04 01:21:55 +09:00
nymkappa
b60c2a9341 Improve disk cache logging 2022-06-03 18:00:14 +02:00
wiz
1efac916b7 Merge pull request #1800 from mempool/nymkappa/bugfix/missing-cache-version
Re-added missing cache version
2022-06-04 00:25:38 +09:00
wiz
3202629c44 Merge pull request #1772 from mempool/nymkappa/feature/websocket-block-count
Refactor pool ranking rxjs observable
2022-06-04 00:08:24 +09:00
nymkappa
3bc55d80ce Re-added missing cache version 2022-06-03 17:04:52 +02:00
wiz
89699f9b7e Merge branch 'master' into nymkappa/feature/websocket-block-count 2022-06-03 23:56:52 +09:00
wiz
95dd436be5 Merge pull request #1795 from hunicus/blocks-extras-v2
Update /api/blocks docs
2022-06-03 23:56:38 +09:00
wiz
efede07b5c Merge branch 'master' into blocks-extras-v2 2022-06-03 23:47:43 +09:00
wiz
a8123cddf7 Merge pull request #1792 from mempool/wiz/update-v2.4.0-screenshot
Update dashboard screenshot for v2.4.0 release
2022-06-03 23:47:21 +09:00
wiz
7764cceb86 Merge pull request #1797 from mempool/nymkappa/feature/automatic-disk-cache-reset
Skip blocks disk cache loading after db migration
2022-06-03 23:46:59 +09:00
nymkappa
256dbc8c8e Add disk cache versioning 2022-06-03 13:31:45 +02:00
hunicus
9ff006e61e Update /api/blocks in docs (bisq) 2022-06-02 18:08:09 -04:00
hunicus
9de6c716b7 Update /api/blocks in docs (liquid) 2022-06-02 17:51:23 -04:00
hunicus
538a1b1666 Replace blocks-extras with blocks in docs (bitcoin) 2022-06-02 17:29:13 -04:00
nymkappa
56e996c893 Refactor pool block list observable 2022-06-02 22:01:53 +02:00
nymkappa
429b4f2bc6 Refactor pool ranking rxjs observable 2022-06-02 22:01:53 +02:00
softsimon
3196c188f1 Merge pull request #1794 from mempool/simon/transifex-extract-6-2
Extracting i18n strings
2022-06-02 22:42:50 +04:00
softsimon
672833930d Extracting i18n strings 2022-06-02 22:42:27 +04:00
wiz
74ee35e273 Merge pull request #1793 from mempool/wiz/update-about-page-logos-for-v2.4
Update the Enterprise Sponsor logos on About page
2022-06-03 03:35:19 +09:00
wiz
8095a8a5f5 Fix size of Exodus logo on About page 2022-06-03 03:27:00 +09:00
wiz
ed3a614fb7 Change Gemini logo on About page to dark theme 2022-06-03 03:26:42 +09:00
wiz
cabfdcf49c Re-arrange Enterprise Sponsor logos on About page 2022-06-03 03:17:58 +09:00
wiz
69e1474c53 Update Exodus logo on About page 2022-06-03 03:01:12 +09:00
wiz
11f5056871 Merge pull request #1784 from hunicus/node-version-requirement
Make node 16.15 required not recommended
2022-06-03 02:53:55 +09:00
wiz
10ccad16e9 Update dashboard screenshot for v2.4.0 release
Fixes #1778
2022-06-03 02:48:38 +09:00
wiz
18c1be0bd0 Merge pull request #1790 from mempool/nymkappa/feature/fee-redesign-2
Updated new fee widget design
2022-06-03 01:23:58 +09:00
nymkappa
fe32ef75a2 Updated new fee widget design 2022-06-02 17:44:44 +02:00
wiz
a6517ebdc5 Merge pull request #1779 from mempool/simon/projected-block-transactions-fixes
Minor refactor for projected block transactions
2022-06-02 21:30:57 +09:00
hunicus
83660e9cf3 Make node 16.15 required not recommended 2022-06-01 22:54:46 -04:00
softsimon
f0a2ddf57b Minor refactor for projected block transactions 2022-06-02 01:29:03 +04:00
wiz
ddab579111 Merge pull request #1774 from mononaut/projected-block-overview
Feature: Projected block visualization
2022-06-02 05:35:07 +09:00
wiz
82471073c3 Merge branch 'master' into projected-block-overview 2022-06-02 00:35:07 +09:00
wiz
c9b98ed841 Merge pull request #1766 from mempool/nymkappa/feature/fee-redesign
Rewamp the fee widget
2022-06-02 00:34:57 +09:00
Mononaut
57cecee3af Add contributors agreement for mononaut 2022-06-01 13:48:58 +00:00
Mononaut
6cd8c448b4 Projected block loading spinner & WebGL detection 2022-06-01 13:48:58 +00:00
Mononaut
3ffc4956f4 Stream projected block deltas instead of full data 2022-06-01 13:48:58 +00:00
Mononaut
c2802253b7 Smarter update algorithm for projected block viz 2022-06-01 13:48:35 +00:00
Mononaut
1aac96a6f6 Projected block overview mouse events & tx preview 2022-06-01 13:48:34 +00:00
Mononaut
d4c9f6decb Implement WebGL projected block visualization 2022-06-01 13:48:34 +00:00
Mononaut
79dae84363 Data pipeline for projected mempool block overview 2022-06-01 13:48:34 +00:00
nymkappa
34576c0609 Update gradient as soon as we receive the fees 2022-06-01 12:53:36 +02:00
nymkappa
ec24549602 Hide minimum and economy on mobile 2022-06-01 12:28:42 +02:00
wiz
7f8834a2eb Merge branch 'master' into nymkappa/feature/fee-redesign 2022-06-01 17:22:01 +09:00
wiz
ee5cd1cd96 Merge pull request #1769 from mempool/nymkappa/bugfix/divergence-fix
Re-add hash field in the mysql block query
2022-06-01 17:20:38 +09:00
wiz
9ab3b3293a Merge pull request #1765 from mempool/simon/init-data-fees
Send fee info with init data
2022-06-01 17:08:09 +09:00
nymkappa
d860344be4 Re-add hash field in the mysql block query 2022-06-01 10:06:18 +02:00
nymkappa
cefc927b06 Tweaks fee widget 2022-06-01 09:47:00 +02:00
wiz
72cc2e4df0 Merge branch 'master' into nymkappa/feature/fee-redesign 2022-06-01 15:14:56 +09:00
nymkappa
b4fd98f565 Rewamp the fee widget 2022-05-31 22:31:01 +02:00
softsimon
2ee1f197d1 Send fee info with init data 2022-06-01 00:03:25 +04:00
wiz
e629173304 Merge pull request #1642 from mempool/nymkappa/bugfix/bisq-dump-file
If bisq data folder is not ready, retry every 3 minutes instead of exit
2022-06-01 03:58:24 +09:00
wiz
6210936ef4 Merge pull request #1752 from mempool/wiz/use-svg-logo-for-mempool.space
Use inline SVG for mempool.space logo
2022-06-01 03:52:40 +09:00
wiz
de8a51fe9a Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 03:47:12 +09:00
wiz
260f883d02 Merge pull request #1764 from mempool/nymkappa/bugfix/fx-rate-db-disable
Only attempt to save fx rate if database is enabled
2022-06-01 03:47:05 +09:00
softsimon
e81dfbcc7f Adding missing ngIf check 2022-05-31 22:39:15 +04:00
nymkappa
c7e9b47aa0 Only attempt to save fx rate if database is enabled 2022-05-31 20:29:43 +02:00
wiz
7f6ea58c74 Merge branch 'master' into wiz/use-svg-logo-for-mempool.space 2022-06-01 03:22:52 +09:00
softsimon
5dcde1c702 Store SVG images in a separate component 2022-05-31 22:21:59 +04:00
wiz
4c90d8e811 Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 03:10:50 +09:00
wiz
dbce727695 Merge pull request #1741 from mempool/nymkappa/feature/remove-fee-calculation-frontend
Remove fee calculation from the frontend
2022-06-01 03:03:09 +09:00
wiz
39f33dded2 Merge branch 'master' into nymkappa/feature/remove-fee-calculation-frontend 2022-06-01 02:44:48 +09:00
wiz
4b445b1191 Merge pull request #1763 from mempool/wiz/fix-backend-starting-syslog-msg
Change backend start syslog message from DEBUG to NOTICE
2022-06-01 02:43:25 +09:00
wiz
a9515f8fc1 Merge pull request #1751 from mempool/simon/handle-nonstandard-inputs
Handle nonstandard inputs
2022-06-01 02:42:57 +09:00
wiz
4ccd786fe9 Change backend start syslog message from DEBUG to NOTICE 2022-06-01 01:09:08 +09:00
wiz
72c3eea863 Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 00:14:43 +09:00
wiz
6b2b10960a Merge branch 'master' into nymkappa/feature/remove-fee-calculation-frontend 2022-05-31 18:34:49 +09:00
nymkappa
f20cf266b6 Remove frontend fee calculation and read it from the websocket instead 2022-05-31 10:41:43 +02:00
wiz
82161d4edf Use SVG logo for mempool.space 2022-05-31 17:31:31 +09:00
softsimon
85b17927d6 Handle nonstandard inputs
fixes #1744
2022-05-31 04:13:13 +04:00
nymkappa
7e8e4b1e6c If bisq data folder is not ready, retry every 3 minutes instead of exit 2022-05-13 11:54:52 +02:00
108 changed files with 12133 additions and 6512 deletions

View File

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

6
.github/pull_request_template.md vendored Normal file
View File

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

View File

@@ -1,10 +1,6 @@
name: Cypress Tests
on:
push:
branches:
- master
pull_request:
on: [push, pull_request]
jobs:
cypress:
runs-on: ${{ matrix.os }}
@@ -25,7 +21,7 @@ jobs:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Mempool)
uses: cypress-io/github-action@v2
uses: cypress-io/github-action@v4
with:
tag: ${{ github.event_name }}
working-directory: frontend
@@ -36,9 +32,9 @@ jobs:
record: true
parallel: true
spec: |
cypress/integration/mainnet/*.spec.ts
cypress/integration/signet/*.spec.ts
cypress/integration/testnet/*.spec.ts
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet/*.spec.ts
group: Tests on ${{ matrix.browser }} (Mempool)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
@@ -49,7 +45,7 @@ jobs:
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- name: ${{ matrix.browser }} browser tests (Liquid)
uses: cypress-io/github-action@v2
uses: cypress-io/github-action@v4
if: always()
with:
tag: ${{ github.event_name }}
@@ -61,8 +57,8 @@ jobs:
record: true
parallel: true
spec: |
cypress/integration/liquid/liquid.spec.ts
cypress/integration/liquidtestnet/liquidtestnet.spec.ts
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on ${{ matrix.browser }} (Liquid)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'
@@ -73,7 +69,7 @@ jobs:
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
- name: ${{ matrix.browser }} browser tests (Bisq)
uses: cypress-io/github-action@v2
uses: cypress-io/github-action@v4
if: always()
with:
tag: ${{ github.event_name }}
@@ -84,7 +80,7 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: cypress/integration/bisq/bisq.spec.ts
spec: cypress/e2e/bisq/bisq.spec.ts
group: Tests on ${{ matrix.browser }} (Bisq)
browser: ${{ matrix.browser }}
ci-build-id: '${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}'

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Node.js 16 and npm 7 are recommended._
_Make sure to use Node.js 16.15 and npm 7._
Install dependencies with `npm` and build the backend:

View File

@@ -15,9 +15,10 @@
"INDEXING_BLOCKS_AMOUNT": 11000,
"PRICE_FEED_UPDATE_INTERVAL": 600,
"USE_SECOND_NODE_FOR_MINFEE": false,
"EXTERNAL_ASSETS": [
"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"
],
"EXTERNAL_ASSETS": [],
"EXTERNAL_MAX_RETRY": 1,
"EXTERNAL_RETRY_INTERVAL": 0,
"USER_AGENT": "mempool",
"STDOUT_LOG_MIN_PRIORITY": "debug"
},
"CORE_RPC": {
@@ -66,6 +67,7 @@
},
"SOCKS5PROXY": {
"ENABLED": false,
"USE_ONION": true,
"HOST": "127.0.0.1",
"PORT": 9050,
"USERNAME": "",
@@ -74,5 +76,13 @@
"PRICE_DATA_SERVER": {
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "https://mempool.space/api/v1",
"MEMPOOL_ONION": "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1",
"LIQUID_API": "https://liquid.network/api/v1",
"LIQUID_ONION": "http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1",
"BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
}
}

View File

@@ -1,10 +1,14 @@
import config from '../../config';
import * as fs from 'fs';
import axios from 'axios';
import axios, { AxiosResponse } from 'axios';
import * as http from 'http';
import * as https from 'https';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
import { Common } from '../common';
import { BlockExtended } from '../../mempool.interfaces';
import { StaticPool } from 'node-worker-threads-pool';
import backendInfo from '../backend-info';
import logger from '../../logger';
class Bisq {
@@ -35,7 +39,13 @@ class Bisq {
constructor() {}
startBisqService(): void {
this.checkForBisqDataFolder();
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.loadBisqDumpFile();
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
@@ -90,7 +100,7 @@ class Bisq {
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
return process.exit(1);
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
}
}
@@ -137,12 +147,59 @@ class Bisq {
}, 2000);
});
}
private async updatePrice() {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpAgent?: http.Agent;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const BISQ_URL = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.EXTERNAL_DATA_SERVER.BISQ_ONION : config.EXTERNAL_DATA_SERVER.BISQ_URL;
const isHTTP = (new URL(BISQ_URL).protocol.split(':')[0] === 'http') ? true : false;
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
private updatePrice() {
axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc', { timeout: 10000 })
.then((response) => {
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
// Handle proxy agent for onion addresses
if (isHTTP) {
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
} else {
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
}
const data: AxiosResponse = await axios.get(`${BISQ_URL}/trades/?market=bsq_btc`, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Bisq market, Error: ${data.status}`);
}
const prices: number[] = [];
response.data.forEach((trade) => {
data.data.forEach((trade) => {
prices.push(parseFloat(trade.price) * 100000000);
});
prices.sort((a, b) => a - b);
@@ -150,9 +207,14 @@ class Bisq {
if (this.priceUpdateCallbackFunction) {
this.priceUpdateCallbackFunction(this.price);
}
}).catch((err) => {
logger.err('Error updating Bisq market price: ' + err);
});
logger.debug('Successfully updated Bisq market price');
break;
} catch (e) {
logger.err('Error updating Bisq market price: ' + (e instanceof Error ? e.message : e));
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
retry++;
}
}
}
private async loadBisqDumpFile(): Promise<void> {
@@ -162,7 +224,7 @@ class Bisq {
this.buildIndex();
this.calculateStats();
} catch (e) {
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
}
}

View File

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

View File

@@ -18,6 +18,7 @@ import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer';
import fiatConversion from './fiat-conversion';
import RatesRepository from '../repositories/RatesRepository';
import poolsParser from './pools-parser';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -139,7 +140,11 @@ class Blocks {
if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
} else {
pool = await poolsRepository.$getUnknownPool();
if (config.DATABASE.ENABLED === true) {
pool = await poolsRepository.$getUnknownPool();
} else {
pool = poolsParser.unknownPool;
}
}
if (!pool) { // We should never have this situation in practise
@@ -165,13 +170,22 @@ class Blocks {
*/
private async $findBlockMiner(txMinerInfo: TransactionMinerInfo | undefined): Promise<PoolTag> {
if (txMinerInfo === undefined || txMinerInfo.vout.length < 1) {
return await poolsRepository.$getUnknownPool();
if (config.DATABASE.ENABLED === true) {
return await poolsRepository.$getUnknownPool();
} else {
return poolsParser.unknownPool;
}
}
const asciiScriptSig = transactionUtils.hex2ascii(txMinerInfo.vin[0].scriptsig);
const address = txMinerInfo.vout[0].scriptpubkey_address;
const pools: PoolTag[] = await poolsRepository.$getPools();
let pools: PoolTag[] = [];
if (config.DATABASE.ENABLED === true) {
pools = await poolsRepository.$getPools();
} else {
pools = poolsParser.miningPools;
}
for (let i = 0; i < pools.length; ++i) {
if (address !== undefined) {
const addresses: string[] = JSON.parse(pools[i].addresses);
@@ -190,7 +204,11 @@ class Blocks {
}
}
return await poolsRepository.$getUnknownPool();
if (config.DATABASE.ENABLED === true) {
return await poolsRepository.$getUnknownPool();
} else {
return poolsParser.unknownPool;
}
}
/**
@@ -343,7 +361,7 @@ class Blocks {
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
}
if (fiatConversion.ratesInitialized === true) {
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
}

View File

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

View File

@@ -1,4 +1,6 @@
import logger from '../logger';
import * as http from 'http';
import * as https from 'https';
import axios, { AxiosResponse } from 'axios';
import { IConversionRates } from '../mempool.interfaces';
import config from '../config';
@@ -25,9 +27,10 @@ class FiatConversion {
}
public startService() {
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
logger.info('Starting currency rates service');
if (config.SOCKS5PROXY.ENABLED) {
logger.info(`Currency rates service will be queried over the Tor network using ${config.PRICE_DATA_SERVER.TOR_URL}`);
logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
} else {
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
}
@@ -40,49 +43,79 @@ class FiatConversion {
}
private async updateCurrency(): Promise<void> {
const headers = { 'User-Agent': `mempool/v${backendInfo.getBackendInfo().version}` };
let fiatConversionUrl: string;
let response: AxiosResponse;
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpAgent?: http.Agent;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
try {
if (config.SOCKS5PROXY.ENABLED) {
let socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
let retry = 0;
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
if (config.SOCKS5PROXY.ENABLED) {
let socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
// Handle proxy agent for onion addresses
if (isHTTP) {
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
} else {
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
}
logger.debug('Querying currency rates service...');
const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
if (response.statusText === 'error' || !response.data) {
throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
}
const agent = new SocksProxyAgent(socksOptions);
fiatConversionUrl = config.PRICE_DATA_SERVER.TOR_URL;
logger.debug('Querying currency rates service...');
response = await axios.get(fiatConversionUrl, { httpAgent: agent, headers: headers, timeout: 30000 });
} else {
fiatConversionUrl = config.PRICE_DATA_SERVER.CLEARNET_URL;
logger.debug('Querying currency rates service...');
response = await axios.get(fiatConversionUrl, { headers: headers, timeout: 10000 });
}
for (const rate of response.data.data) {
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
for (const rate of response.data.data) {
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
}
}
}
this.ratesInitialized = true;
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
this.ratesInitialized = true;
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.conversionRates);
}
break;
} catch (e) {
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
retry++;
}
} catch (e) {
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
}
}
}

View File

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

View File

@@ -11,6 +11,14 @@ interface Pool {
}
class PoolsParser {
miningPools: any[] = [];
unknownPool: any = {
'name': "Unknown",
'link': "https://learnmeabitcoin.com/technical/coinbase-transaction",
'regexes': "[]",
'addresses': "[]",
'slug': 'unknown'
};
slugWarnFlag = false;
/**
@@ -60,12 +68,18 @@ class PoolsParser {
// Get existing pools from the db
let existingPools;
try {
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
if (config.DATABASE.ENABLED === true) {
[existingPools] = await DB.query({ sql: 'SELECT * FROM pools;', timeout: 120000 });
} else {
existingPools = [];
}
} catch (e) {
logger.err('Cannot get existing pools from the database, skipping pools.json import');
return;
}
this.miningPools = [];
// Finally, we generate the final consolidated pools data
const finalPoolDataAdd: Pool[] = [];
const finalPoolDataUpdate: Pool[] = [];
@@ -97,24 +111,33 @@ class PoolsParser {
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`);
}
const poolObj = {
'name': finalPoolName,
'link': match[0].link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
};
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
finalPoolDataUpdate.push({
'name': finalPoolName,
'link': match[0].link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
});
finalPoolDataUpdate.push(poolObj);
} else {
logger.debug(`Add '${finalPoolName}' mining pool`);
finalPoolDataAdd.push({
'name': finalPoolName,
'link': match[0].link,
'regexes': allRegexes,
'addresses': allAddresses,
'slug': slug
});
finalPoolDataAdd.push(poolObj);
}
this.miningPools.push({
'name': finalPoolName,
'link': match[0].link,
'regexes': JSON.stringify(allRegexes),
'addresses': JSON.stringify(allAddresses),
'slug': slug
});
}
if (config.DATABASE.ENABLED === false) { // Don't run db operations
logger.info('Mining pools.json import completed (no database)');
return;
}
logger.debug(`Update pools table now`);
@@ -128,7 +151,7 @@ class PoolsParser {
}
queryAdd = queryAdd.slice(0, -1) + ';';
// Add new mining pools into the database
// Updated existing mining pools in the database
const updateQueries: string[] = [];
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
updateQueries.push(`

View File

@@ -1,6 +1,6 @@
import logger from '../logger';
import * as WebSocket from 'ws';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -13,6 +13,7 @@ import config from '../config';
import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -110,6 +111,20 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
response['projected-block-transactions'] = {
index: index,
blockTransactions: mBlocksWithTransactions[index]?.transactions || [],
};
} else {
client['track-mempool-block'] = null;
}
}
if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
if (!_blocks) {
@@ -199,6 +214,7 @@ class WebsocketHandler {
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties
};
}
@@ -231,11 +247,13 @@ class WebsocketHandler {
mempoolBlocks.updateMempoolBlocks(newMempool);
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions);
const recommendedFees = feeApi.getRecommendedFee();
this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
@@ -249,6 +267,7 @@ class WebsocketHandler {
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
response['da'] = da;
response['fees'] = recommendedFees;
}
if (client['want-mempool-blocks']) {
@@ -366,6 +385,16 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
}
@@ -378,6 +407,7 @@ class WebsocketHandler {
}
let mBlocks: undefined | MempoolBlock[];
let mBlockDeltas: undefined | MempoolBlockDelta[];
let matchRate = 0;
const _memPool = memPool.getMempool();
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
@@ -394,12 +424,16 @@ class WebsocketHandler {
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
}
if (block.extras) {
block.extras.matchRate = matchRate;
}
const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee();
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -412,7 +446,8 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'da': da,
'fees': fees,
};
if (mBlocks && client['want-mempool-blocks']) {
@@ -487,6 +522,16 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
client.send(JSON.stringify(response));
});
}

View File

@@ -18,6 +18,9 @@ interface IConfig {
PRICE_FEED_UPDATE_INTERVAL: number;
USE_SECOND_NODE_FOR_MINFEE: boolean;
EXTERNAL_ASSETS: string[];
EXTERNAL_MAX_RETRY: number;
EXTERNAL_RETRY_INTERVAL: number;
USER_AGENT: string;
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
};
ESPLORA: {
@@ -66,6 +69,7 @@ interface IConfig {
};
SOCKS5PROXY: {
ENABLED: boolean;
USE_ONION: boolean;
HOST: string;
PORT: number;
USERNAME: string;
@@ -75,6 +79,14 @@ interface IConfig {
TOR_URL: string;
CLEARNET_URL: string;
};
EXTERNAL_DATA_SERVER: {
MEMPOOL_API: string;
MEMPOOL_ONION: string;
LIQUID_API: string;
LIQUID_ONION: string;
BISQ_URL: string;
BISQ_ONION: string;
};
}
const defaults: IConfig = {
@@ -94,9 +106,10 @@ const defaults: IConfig = {
'INDEXING_BLOCKS_AMOUNT': 11000, // 0 = disable indexing, -1 = index all blocks
'PRICE_FEED_UPDATE_INTERVAL': 600,
'USE_SECOND_NODE_FOR_MINFEE': false,
'EXTERNAL_ASSETS': [
'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json'
],
'EXTERNAL_ASSETS': [],
'EXTERNAL_MAX_RETRY': 1,
'EXTERNAL_RETRY_INTERVAL': 0,
'USER_AGENT': 'mempool',
'STDOUT_LOG_MIN_PRIORITY': 'debug',
},
'ESPLORA': {
@@ -145,6 +158,7 @@ const defaults: IConfig = {
},
'SOCKS5PROXY': {
'ENABLED': false,
'USE_ONION': true,
'HOST': '127.0.0.1',
'PORT': 9050,
'USERNAME': '',
@@ -153,6 +167,14 @@ const defaults: IConfig = {
"PRICE_DATA_SERVER": {
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
},
"EXTERNAL_DATA_SERVER": {
'MEMPOOL_API': 'https://mempool.space/api/v1',
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
'LIQUID_API': 'https://liquid.network/api/v1',
'LIQUID_ONION': 'http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1',
'BISQ_URL': 'https://bisq.markets/api',
'BISQ_ONION': 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
}
};
@@ -168,6 +190,7 @@ class Config implements IConfig {
BISQ: IConfig['BISQ'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
constructor() {
const configs = this.merge(configFile, defaults);
@@ -182,6 +205,7 @@ class Config implements IConfig {
this.BISQ = configs.BISQ;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -22,12 +22,20 @@ import { PoolOptions } from 'mysql2/typings/mysql';
timezone: '+00:00',
};
private checkDBFlag() {
if (config.DATABASE.ENABLED === false) {
logger.err('Trying to use DB feature but config.DATABASE.ENABLED is set to false, please open an issue');
}
}
public async query(query, params?) {
this.checkDBFlag();
const pool = await this.getPool();
return pool.query(query, params);
}
public async checkDbConnection() {
this.checkDBFlag();
try {
await this.query('SELECT ?', [1]);
logger.info('Database connection established.');

View File

@@ -67,7 +67,7 @@ class Server {
}
async startServer(worker = false) {
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
this.app
.use((req: Request, res: Response, next: NextFunction) => {
@@ -205,7 +205,7 @@ class Server {
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', routes.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -213,7 +213,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
@@ -223,7 +223,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/contributors', { responseType: 'stream', timeout: 10000 });
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -231,7 +231,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/contributors/images/' + req.params.id, {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
@@ -241,7 +241,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/translators', { responseType: 'stream', timeout: 10000 });
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -249,7 +249,7 @@ class Server {
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/translators/images/' + req.params.id, {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);

View File

@@ -13,7 +13,9 @@ class Indexer {
}
public reindex() {
this.runIndexer = true;
if (Common.indexingEnabled()) {
this.runIndexer = true;
}
}
public async $run() {

View File

@@ -33,6 +33,12 @@ export interface MempoolBlock {
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
transactions: TransactionStripped[];
}
export interface MempoolBlockDelta {
added: TransactionStripped[];
removed: string[];
}
interface VinStrippedToScriptsig {

View File

@@ -307,6 +307,7 @@ class BlocksRepository {
try {
const [rows]: any[] = await DB.query(`SELECT
height,
hash,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,

View File

@@ -990,7 +990,7 @@ class Routes {
public async $getAllFeaturedLiquidAssets(req: Request, res: Response) {
try {
const response = await axios.get('https://liquid.network/api/v1/assets/featured', { responseType: 'stream', timeout: 10000 });
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/featured`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
@@ -999,7 +999,7 @@ class Routes {
public async $getAssetGroup(req: Request, res: Response) {
try {
const response = await axios.get('https://liquid.network/api/v1/assets/group/' + parseInt(req.params.id, 10),
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.LIQUID_API}/assets/group/${parseInt(req.params.id, 10)}`,
{ responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {

View File

@@ -1,6 +1,7 @@
import axios, { AxiosResponse } from 'axios';
import * as fs from 'fs';
import config from './config';
import backendInfo from './api/backend-info';
import logger from './logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
@@ -42,6 +43,9 @@ class SyncAssets {
logger.info(`Downloading external asset ${fileName} over the Tor network...`);
return axios.get(url, {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
httpAgent: agent,
httpsAgent: agent,
responseType: 'stream',
@@ -57,6 +61,9 @@ class SyncAssets {
} else {
logger.info(`Downloading external asset ${fileName} over clearnet...`);
return axios.get(url, {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
responseType: 'stream',
timeout: 30000
}).then(function (response) {

View File

@@ -1,7 +1,8 @@
import axios from 'axios';
import axios, { AxiosResponse } from 'axios';
import poolsParser from '../api/pools-parser';
import config from '../config';
import DB from '../database';
import backendInfo from '../api/backend-info';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
@@ -11,12 +12,13 @@ import * as https from 'https';
*/
class PoolsUpdater {
lastRun: number = 0;
currentSha: any = undefined;
constructor() {
}
public async updatePoolsJson() {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.DATABASE.ENABLED === false) {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return;
}
@@ -38,14 +40,17 @@ class PoolsUpdater {
}
try {
const dbSha = await this.getShaFromDb();
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
if (githubSha === undefined) {
return;
}
logger.debug(`Pools.json sha | Current: ${dbSha} | Github: ${githubSha}`);
if (dbSha !== undefined && dbSha === githubSha) {
if (config.DATABASE.ENABLED === true) {
this.currentSha = await this.getShaFromDb();
}
logger.debug(`Pools.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
if (this.currentSha !== undefined && this.currentSha === githubSha) {
return;
}
@@ -68,12 +73,14 @@ class PoolsUpdater {
* Fetch our latest pools.json sha from the db
*/
private async updateDBSha(githubSha: string) {
try {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
return undefined;
this.currentSha = githubSha;
if (config.DATABASE.ENABLED === true) {
try {
await DB.query('DELETE FROM state where name="pools_json_sha"');
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
} catch (e) {
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
}
}
}
@@ -113,33 +120,45 @@ class PoolsUpdater {
*/
private async query(path): Promise<object | undefined> {
type axiosOptions = {
headers: {
'User-Agent': string
};
timeout: number;
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const axiosOptions: axiosOptions = {};
const axiosOptions: axiosOptions = {
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
};
let retry = 0;
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
}
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
while(retry < 5) {
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
try {
const data = await axios.get(path, axiosOptions);
if (data.statusText !== 'OK' || !data.data) {
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
} else {
// Retry with different tor circuits https://stackoverflow.com/a/64960234
socksOptions.username = `circuit${retry}`;
}
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
const data: AxiosResponse = await axios.get(path, axiosOptions);
if (data.statusText === 'error' || !data.data) {
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
}
return data.data;
@@ -147,7 +166,7 @@ class PoolsUpdater {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay();
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
}
return undefined;
}

View File

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

View File

@@ -15,6 +15,9 @@
"PRICE_FEED_UPDATE_INTERVAL": __MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__,
"USE_SECOND_NODE_FOR_MINFEE": __MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__,
"EXTERNAL_ASSETS": __MEMPOOL_EXTERNAL_ASSETS__,
"EXTERNAL_MAX_RETRY": __MEMPOOL_EXTERNAL_MAX_RETRY__,
"EXTERNAL_RETRY_INTERVAL": __MEMPOOL_EXTERNAL_RETRY_INTERVAL__,
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__
},
@@ -64,6 +67,7 @@
},
"SOCKS5PROXY": {
"ENABLED": __SOCKS5PROXY_ENABLED__,
"USE_ONION": __SOCKS5PROXY_USE_ONION__,
"HOST": "__SOCKS5PROXY_HOST__",
"PORT": "__SOCKS5PROXY_PORT__",
"USERNAME": "__SOCKS5PROXY_USERNAME__",
@@ -72,5 +76,13 @@
"PRICE_DATA_SERVER": {
"TOR_URL": "__PRICE_DATA_SERVER_TOR_URL__",
"CLEARNET_URL": "__PRICE_DATA_SERVER_CLEARNET_URL__"
},
"EXTERNAL_DATA_SERVER": {
"MEMPOOL_API": "__EXTERNAL_DATA_SERVER_MEMPOOL_API__",
"MEMPOOL_ONION": "__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__",
"LIQUID_API": "__EXTERNAL_DATA_SERVER_LIQUID_API__",
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
}
}

View File

@@ -16,7 +16,10 @@ __MEMPOOL_MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_MEMPOOL_BLOCKS_AMOUNT:=8}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=11000}
__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__=${MEMPOOL_PRICE_FEED_UPDATE_INTERVAL:=600}
__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__=${MEMPOOL_USE_SECOND_NODE_FOR_MINFEE:=false}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[\"https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json\"]}
__MEMPOOL_EXTERNAL_ASSETS__=${MEMPOOL_EXTERNAL_ASSETS:=[]}
__MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
# CORE_RPC
@@ -65,6 +68,7 @@ __BISQ_DATA_PATH__=${BISQ_DATA_PATH:=/bisq/statsnode-data/btc_mainnet/db}
# SOCKS5PROXY
__SOCKS5PROXY_ENABLED__=${SOCKS5PROXY_ENABLED:=false}
__SOCKS5PROXY_USE_ONION__=${SOCKS5PROXY_USE_ONION:=true}
__SOCKS5PROXY_HOST__=${SOCKS5PROXY_HOST:=localhost}
__SOCKS5PROXY_PORT__=${SOCKS5PROXY_PORT:=9050}
__SOCKS5PROXY_USERNAME__=${SOCKS5PROXY_USERNAME:=""}
@@ -74,6 +78,14 @@ __SOCKS5PROXY_PASSWORD__=${SOCKS5PROXY_PASSWORD:=""}
__PRICE_DATA_SERVER_TOR_URL__=${PRICE_DATA_SERVER_TOR_URL:=http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices}
__PRICE_DATA_SERVER_CLEARNET_URL__=${PRICE_DATA_SERVER_CLEARNET_URL:=https://price.bisq.wiz.biz/getAllMarketPrices}
# EXTERNAL_DATA_SERVER
__EXTERNAL_DATA_SERVER_MEMPOOL_API__=${EXTERNAL_DATA_SERVER_MEMPOOL_API:=https://mempool.space/api/v1}
__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__=${EXTERNAL_DATA_SERVER_MEMPOOL_ONION:=http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_API__=${EXTERNAL_DATA_SERVER_LIQUID_API:=https://liquid.network/api/v1}
__EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http://liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd.onion/api/v1}
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@@ -92,6 +104,9 @@ sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT_
sed -i "s/__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__/${__MEMPOOL_PRICE_FEED_UPDATE_INTERVAL__}/g" mempool-config.json
sed -i "s/__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__/${__MEMPOOL_USE_SECOND_NODE_FOR_MINFEE__}/g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_ASSETS__!${__MEMPOOL_EXTERNAL_ASSETS__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" mempool-config.json
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_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/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
@@ -132,6 +147,7 @@ sed -i "s/__BISQ_ENABLED__/${__BISQ_ENABLED__}/g" mempool-config.json
sed -i "s!__BISQ_DATA_PATH__!${__BISQ_DATA_PATH__}!g" mempool-config.json
sed -i "s/__SOCKS5PROXY_ENABLED__/${__SOCKS5PROXY_ENABLED__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USE_ONION__/${__SOCKS5PROXY_USE_ONION__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_HOST__/${__SOCKS5PROXY_HOST__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_PORT__/${__SOCKS5PROXY_PORT__}/g" mempool-config.json
sed -i "s/__SOCKS5PROXY_USERNAME__/${__SOCKS5PROXY_USERNAME__}/g" mempool-config.json
@@ -140,4 +156,11 @@ sed -i "s/__SOCKS5PROXY_PASSWORD__/${__SOCKS5PROXY_PASSWORD__}/g" mempool-config
sed -i "s!__PRICE_DATA_SERVER_TOR_URL__!${__PRICE_DATA_SERVER_TOR_URL__}!g" mempool-config.json
sed -i "s!__PRICE_DATA_SERVER_CLEARNET_URL__!${__PRICE_DATA_SERVER_CLEARNET_URL__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_API__!${__EXTERNAL_DATA_SERVER_MEMPOOL_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__!${__EXTERNAL_DATA_SERVER_MEMPOOL_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_API__!${__EXTERNAL_DATA_SERVER_LIQUID_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_ONION__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_API__!${__EXTERNAL_DATA_SERVER_BISQ_API__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
node /backend/dist/index.js

View File

@@ -34,7 +34,7 @@ $ npm run config:defaults:bisq
### 3. Run the Frontend
_Node.js 16 and npm 7 are recommended._
_Make sure to use Node.js 16.15 and npm 7._
Install project dependencies and run the frontend server:

View File

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

View File

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

View File

@@ -35,13 +35,14 @@ describe('Bisq', () => {
"Proposal", "Reimbursement request", "Transfer BSQ", "Unlock", "Vote reveal"
];
filters.forEach((filter) => {
it(`filters the transaction screen by ${filter}`, () => {
it.only(`filters the transaction screen by ${filter}`, () => {
cy.visit(`${basePath}/transactions`);
cy.wait('@txs');
cy.waitForSkeletonGone();
cy.get('#filter').click();
cy.contains(filter).find('input').click();
//TODO: change this waiter
cy.wait(1000);
cy.wait('@txs');
cy.wait(500);
cy.get('td:nth-of-type(2)').each(($td) => {
expect($td.text().trim()).to.eq(filter);
});
@@ -56,7 +57,7 @@ describe('Bisq', () => {
filters.forEach((filter) => {
cy.contains(filter).find('input').click();
//TODO: change this waiter
cy.wait(1000);
cy.wait(1500);
});
cy.get('td:nth-of-type(2)').each(($td) => {
const regex = new RegExp(`${filters.join('|')}`, 'g');

View File

@@ -124,7 +124,7 @@ describe('Liquid', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid Bitcoin').then(() => {
cy.get('ngb-typeahead-window').should('have.length', 1);
cy.get('ngb-typeahead-window', { timeout: 30000 }).should('have.length', 1);
});
});
@@ -132,7 +132,7 @@ describe('Liquid', () => {
cy.visit(`${basePath}/assets`);
cy.waitForSkeletonGone();
cy.get('.container-xl input').click().type('Liquid AUD').then(() => {
cy.get('ngb-typeahead-window:nth-of-type(1) button').click();
cy.get('ngb-typeahead-window:nth-of-type(1) button', { timeout: 30000 }).click();
});
});
});

View File

@@ -189,7 +189,7 @@ describe('Mainnet', () => {
cy.get('[data-cy="tx-2"] .table-tx-vin .highlight').invoke('text').should('contain', `${address}`);
});
it.only('highlights both input and output addresses in the same transaction', () => {
it('highlights both input and output addresses in the same transaction', () => {
const address = 'bc1q03u63r6hm7a3v6em58zdqtp446w2pw30nm63mv';
cy.visit(`/address/${address}`);
cy.waitForSkeletonGone();
@@ -241,7 +241,7 @@ describe('Mainnet', () => {
cy.get('[ngbtooltip="Next Block"] > .ng-fa-icon > .svg-inline--fa').should('not.exist');
cy.get('[ngbtooltip="Previous Block"] > .ng-fa-icon > .svg-inline--fa').should('be.visible');
cy.document().left();
cy.get('.title-block h1').invoke('text').should('equal', 'Next block');
cy.get('.title-block h1').invoke('text').should('equal', 'Next Block');
});
});

View File

@@ -50,98 +50,98 @@ import { mockWebSocket } from './websocket';
/* global Cypress */
const codes = {
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40
}
Cypress.Commands.add('waitForSkeletonGone', () => {
cy.waitUntil(() => {
return Cypress.$('.skeleton-loader').length === 0;
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50});
cy.waitUntil(() => {
return Cypress.$('.skeleton-loader').length === 0;
}, { verbose: true, description: "waitForSkeletonGone", errorMsg: "skeleton loaders never went away", timeout: 15000, interval: 50 });
});
Cypress.Commands.add(
"waitForPageIdle",
() => {
console.warn("Waiting for page idle state");
const pageIdleDetector = new PageIdleDetector();
pageIdleDetector.WaitForPageToBeIdle();
}
"waitForPageIdle",
() => {
console.warn("Waiting for page idle state");
const pageIdleDetector = new PageIdleDetector();
pageIdleDetector.WaitForPageToBeIdle();
}
);
Cypress.Commands.add('mockMempoolSocket', () => {
mockWebSocket();
});
Cypress.Commands.add('changeNetwork', (network: "testnet"|"signet"|"liquid"|"bisq"|"mainnet" ) => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`.${network}`).click().then(() => {
cy.waitForPageIdle();
if(network !== 'bisq'){
cy.waitForSkeletonGone();
}
});
Cypress.Commands.add('changeNetwork', (network: "testnet" | "signet" | "liquid" | "bisq" | "mainnet") => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`a.${network}`).click().then(() => {
cy.waitForPageIdle();
if (network !== 'bisq') {
cy.waitForSkeletonGone();
}
});
});
});
// https://github.com/bahmutov/cypress-arrows/blob/8f0303842a343550fbeaf01528d01d1ff213b70c/src/index.js
function keydownCommand ($el, key) {
const message = `sending the "${key}" keydown event`
const log = Cypress.log({
name: `keydown: ${key}`,
message: message,
consoleProps: function () {
return {
Subject: $el
}
function keydownCommand($el, key) {
const message = `sending the "${key}" keydown event`
const log = Cypress.log({
name: `keydown: ${key}`,
message: message,
consoleProps: function () {
return {
Subject: $el
}
})
const e = $el.createEvent('KeyboardEvent')
Object.defineProperty(e, 'key', {
get: function () {
return key
}
})
Object.defineProperty(e, 'keyCode', {
get: function () {
return this.keyCodeVal
}
})
Object.defineProperty(e, 'which', {
get: function () {
return this.keyCodeVal
}
})
var metaKey = false
Object.defineProperty(e, 'metaKey', {
get: function () {
return metaKey
}
})
Object.defineProperty(e, 'shiftKey', {
get: function () {
return false
}
})
e.keyCodeVal = codes[key]
e.initKeyboardEvent('keydown', true, true,
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
$el.dispatchEvent(e)
log.snapshot().end()
return $el
}
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))
}
})
const e = $el.createEvent('KeyboardEvent')
Object.defineProperty(e, 'key', {
get: function () {
return key
}
})
Object.defineProperty(e, 'keyCode', {
get: function () {
return this.keyCodeVal
}
})
Object.defineProperty(e, 'which', {
get: function () {
return this.keyCodeVal
}
})
var metaKey = false
Object.defineProperty(e, 'metaKey', {
get: function () {
return metaKey
}
})
Object.defineProperty(e, 'shiftKey', {
get: function () {
return false
}
})
e.keyCodeVal = codes[key]
e.initKeyboardEvent('keydown', true, true,
$el.defaultView, false, false, false, false, e.keyCodeVal, e.keyCodeVal)
$el.dispatchEvent(e)
log.snapshot().end()
return $el
}
Cypress.Commands.add('keydown', { prevSubject: "dom" }, keydownCommand)
Cypress.Commands.add('left', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowLeft'))
Cypress.Commands.add('right', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowRight'))
Cypress.Commands.add('up', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowUp'))
Cypress.Commands.add('down', { prevSubject: "dom" }, $el => keydownCommand($el, 'ArrowDown'))

View File

@@ -70,7 +70,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.6.1",
"cypress": "^10.0.2",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",
@@ -6901,9 +6901,9 @@
"devOptional": true
},
"node_modules/cypress": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz",
"integrity": "sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -22471,9 +22471,9 @@
"devOptional": true
},
"cypress": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.0.2.tgz",
"integrity": "sha512-7+C4KHYBcfZrawss+Gt5PlS35rfc6ySc59JcHDVsIMm1E/J35dqE41UEXpdtwIq3549umCerNWnFADzqib4kcA==",
"optional": true,
"requires": {
"@cypress/request": "^2.88.10",

View File

@@ -122,7 +122,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.6.1",
"cypress": "^10.0.2",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",

View File

@@ -47,62 +47,19 @@
</svg>
<span>Spiral</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: white" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<defs>
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
<stop stop-color="#00BFFF" offset="0%"></stop>
<stop stop-color="#6619FF" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>
<span>Exodus</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect fill="#87E1A1" fill-rule="nonzero" x="0" y="0" width="400" height="400"></rect>
<path d="M124,149.256434 L169.106586,149.256434 L169.106586,128.378728 C169.106586,102.958946 183.316852,90 207.489341,90 L276.773787,90 L276.773787,119.404671 L222.192348,119.404671 C216.458028,119.404671 213.968815,122.397366 213.968815,127.633575 L213.968815,149.256434 L276.023264,149.256434 L276.023264,181.902184 L213.968815,181.902184 L213.968815,310 L169.106586,310 L169.106586,181.902184 L124,181.902184 L124,149.256434" fill="#000000"></path>
<g transform="translate(-186.000000, -2316.000000)">
<g transform="translate(186.000000, 2316.000000)">
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
</g>
</g>
</g>
</svg>
<span>Foundry</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span>
</a>
<a href="https://blockstream.com/" target="_blank" title="Blockstream">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
<style type="text/css">
@@ -149,6 +106,76 @@
</svg>
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
<path d="M150.638 271.648H229.168V229.351H150.346L140.128 196.097L229.168 138.048V87L89.9159 178.303L112.687 250.646L89.9159 322.989L229.459 414V362.952L140.128 304.903L150.638 271.648Z" fill="url(#paint1_linear_2_14)"/>
<mask id="mask0_2_14" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="89" y="87" width="323" height="327">
<path d="M411.042 178.303L271.79 87.0001V138.048L361.121 196.097L350.612 229.352H271.79V271.649H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint2_linear_2_14)"/>
<path d="M150.638 271.649H229.168V229.352H150.346L140.128 196.097L229.168 138.048V87.0001L89.9161 178.303L112.687 250.646L89.9161 322.989L229.46 414V362.952L140.128 304.903L150.638 271.649Z" fill="url(#paint3_linear_2_14)"/>
</mask>
<g mask="url(#mask0_2_14)">
<path d="M408.913 87.0001H90.0877V414H408.913V87.0001Z" fill="url(#paint4_linear_2_14)"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint1_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint2_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint3_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint4_linear_2_14" x1="110.525" y1="160.575" x2="271.982" y2="281.156" gradientUnits="userSpaceOnUse">
<stop offset="0.119792" stop-color="#8952FF" stop-opacity="0.87"/>
<stop offset="1" stop-color="#DABDFF" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_2_14">
<rect width="327" height="327" fill="white" transform="translate(86 87)"/>
</clipPath>
</defs>
</svg>
<span>Exodus</span>
</a>
</div>
</div>

View File

@@ -85,6 +85,8 @@ export class AddressLabelsComponent implements OnInit {
}
this.detectMultisig(this.vin.inner_redeemscript_asm);
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
}
detectMultisig(script: string) {
@@ -118,7 +120,11 @@ export class AddressLabelsComponent implements OnInit {
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
if (ops.length) {
return;
}
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`;
}
handleVout() {

View File

@@ -22,7 +22,7 @@
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td class="text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td>
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
<div class="tooltip-custom">

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, timer } from 'rxjs';
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
@@ -15,7 +15,7 @@ import { WebsocketService } from 'src/app/services/websocket.service';
export class BlocksList implements OnInit {
@Input() widget: boolean = false;
blocks$: Observable<any[]> = undefined;
blocks$: Observable<BlockExtended[]> = undefined;
indexingAvailable = false;
isLoading = true;
@@ -27,6 +27,7 @@ export class BlocksList implements OnInit {
blocksCount: number;
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromBlockHeight);
skeletonLines: number[] = [];
lastBlockHeight = -1;
constructor(
private apiService: ApiService,
@@ -57,6 +58,7 @@ export class BlocksList implements OnInit {
this.blocksCount = blocks[0].height + 1;
}
this.isLoading = false;
this.lastBlockHeight = Math.max(...blocks.map(o => o.height))
}),
map(blocks => {
if (this.indexingAvailable) {
@@ -73,12 +75,18 @@ export class BlocksList implements OnInit {
}),
retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
)
})
})
),
this.stateService.blocks$
.pipe(
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
),
switchMap((block) => {
if (block[0].height < this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = block[0].height;
return [block];
})
)
])
.pipe(
scan((acc, blocks) => {

View File

@@ -133,7 +133,7 @@
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
padding: 24px 20px;
}
}
@@ -148,3 +148,7 @@
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
}

View File

@@ -1,44 +1,73 @@
<div class="fee-estimation-wrapper">
<div class="fee-estimation-container" *ngIf="(isLoadingWebSocket$ | async) === false && (feeEstimations$ | async) as feeEstimations; else loadingFees">
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority" i18n-ngbTooltip="Transaction feerate tooltip (economy)" ngbTooltip="Either 2x the minimum, or the Low Priority rate (whichever is lower)" placement="top">No Priority</span>
</div>
<div class="band-separator fill"></div>
<div class="fee-progress-bar priority" [style.background]="gradient">
<span class="fee-label prority" i18n="fees-box.low-priority" i18n-ngbTooltip="Transaction feerate tooltip (low priority)" ngbTooltip="Places your transaction in between the second and third mempool blocks" placement="top">Low Priority</span>
<span class="fee-label prority" i18n="fees-box.medium-priority" i18n-ngbTooltip="Transaction feerate tooltip (medium priority)" ngbTooltip="Places your transaction in between the first and second mempool blocks" placement="top">Medium Priority</span>
<span class="fee-label prority" i18n="fees-box.high-priority" i18n-ngbTooltip="Transaction feerate tooltip (high priority)" ngbTooltip="Places your transaction in the first mempool block" placement="top">High Priority</span>
</div>
</div>
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.hourFee * 140" ></app-fiat></span>
<div class="card-text">
<div class="fee-text">{{ recommendedFees.economyFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.economyFee * 140" ></app-fiat></span>
</div>
</div>
<div class="band-separator"></div>
<div class="item">
<div class="card-text">
<div class="fee-text">{{ recommendedFees.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.hourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.halfHourFee * 140" ></app-fiat></span>
<div class="card-text">
<div class="fee-text">{{ recommendedFees.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.halfHourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.fastestFee * 140" ></app-fiat></span>
<div class="card-text">
<div class="fee-text">{{ recommendedFees.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.fastestFee * 140" ></app-fiat></span>
</div>
</div>
</div>
</div>
<ng-template #loadingFees>
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority">No Priority</span>
</div>
<div class="band-separator fill"></div>
<div class="fee-progress-bar priority" [style.background]="gradient">
<span class="fee-label prority" i18n="fees-box.low-priority">Low Priority</span>
<span class="fee-label prority" i18n="fees-box.medium-priority">Medium Priority</span>
<span class="fee-label prority" i18n="fees-box.high-priority">High Priority</span>
</div>
</div>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="band-separator"></div>
<div class="item">
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@@ -1,7 +1,7 @@
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
margin-bottom: 4px;
font-size: 1rem;
}
@@ -21,28 +21,17 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
flex-direction: row;
.item {
max-width: 150px;
width: 100px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
&:first-child {
@media (767px < width < 992px), (width < 576px) {
display: none
}
}
margin: 0 auto 0px;
&:last-child {
margin-bottom: 0;
}
@@ -55,18 +44,17 @@
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
font-size: 20px;
}
.fiat {
display: block;
font-size: 14px !important;
font-size: 13px !important;
}
}
}
.loading-container{
min-height: 76px;
height: 50px;
}
.card-text {
@@ -74,12 +62,62 @@
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
max-width: 70px;
margin: 10px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.fee-progress-bar {
width: 25%;
height: 22px;
margin-bottom: 12px;
display: flex;
flex-direction: row;
transition: background-color 1s;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;
}
width: 75%;
border-radius: 0px 10px 10px 0px;
}
&:first-child {
@media (767px < width < 992px), (width < 576px) {
display: none
}
}
}
.band-separator {
width: 5%;
@media (767px < width < 992px), (width < 576px) {
display: none
}
&.fill {
height: 22px;
background: repeating-linear-gradient(
90deg,
rgb(45, 51, 72),
rgb(45, 51, 72) 2px,
rgb(29, 31, 49) 2px,
rgb(29, 31, 49) 4px
);
}
}
.fee-label {
padding-top: 2px;
font-size: 12px;
width: 100%;
@media (767px < width < 992px), (width < 576px) {
width: 33%;
}
&.prority {
width: 33%;
}
}

View File

@@ -1,14 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { map, filter } from 'rxjs/operators';
import { merge, Observable } from 'rxjs';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
interface FeeEstimations {
fastestFee: number;
halfHourFee: number;
hourFee: number;
}
import { Observable } from 'rxjs';
import { Recommendedfees } from 'src/app/interfaces/websocket.interface';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-fees-box',
@@ -17,52 +12,32 @@ interface FeeEstimations {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeesBoxComponent implements OnInit {
feeEstimations$: Observable<FeeEstimations>;
isLoadingWebSocket$: Observable<boolean>;
defaultFee: number;
recommendedFees$: Observable<Recommendedfees>;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
constructor(
private stateService: StateService,
private stateService: StateService
) { }
ngOnInit(): void {
this.defaultFee = this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ? 0.1 : 1;
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.feeEstimations$ = this.stateService.mempoolBlocks$
this.recommendedFees$ = this.stateService.recommendedFees$
.pipe(
map((pBlocks) => {
if (!pBlocks.length) {
return {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
};
}
tap((fees) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const firstMedianFee = this.optimizeMedianFee(pBlocks[0], pBlocks[1]);
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
return {
'fastestFee': firstMedianFee,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
};
})
);
this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
this.noPriority = startColor;
}
)
);
}
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
if (pBlock.blockVSize <= 500000) {
return this.defaultFee;
}
if (pBlock.blockVSize <= 950000 && !nextBlock) {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}
return Math.ceil(useFee);
}
}

View File

@@ -3,7 +3,8 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<img *ngIf="!officialMempoolSpace" src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>

View File

@@ -0,0 +1,682 @@
import { FastVertexArray } from './fast-vertex-array'
import TxView from './tx-view'
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Position, Square } from './sprite-types'
export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}};
vertexArray: FastVertexArray;
txs: { [key: string]: TxView };
width: number;
height: number;
gridWidth: number;
gridHeight: number;
gridSize: number;
vbytesPerUnit: number;
unitPadding: number;
unitWidth: number;
initialised: boolean;
layout: BlockLayout;
dirty: boolean;
constructor ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }) {
this.init({ width, height, resolution, blockLimit, vertexArray })
}
destroy (): void {
Object.values(this.txs).forEach(tx => tx.destroy())
}
resize ({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
this.width = width
this.height = height
this.gridSize = this.width / this.gridWidth
this.unitPadding = Math.floor(Math.max(1, width / 1000))
this.unitWidth = this.gridSize - (this.unitPadding * 2)
this.dirty = true
if (this.initialised && this.scene) this.updateAll(performance.now())
}
// Animate new block entering scene
enter (txs: TransactionStripped[], direction) {
this.replace(txs, direction)
}
// Animate block leaving scene
exit (direction: string): void {
const startTime = performance.now()
const removed = this.removeBatch(Object.keys(this.txs), startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 2000)
}
// Reset layout and replace with new set of transactions
replace (txs: TransactionStripped[], direction: string = 'left'): void {
const startTime = performance.now()
const nextIds = {}
const remove = []
txs.forEach(tx => {
nextIds[tx.txid] = true
})
Object.keys(this.txs).forEach(txid => {
if (!nextIds[txid]) remove.push(txid)
})
txs.forEach(tx => {
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
})
const removed = this.removeBatch(remove, startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 1000)
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
this.place(tx)
})
this.updateAll(startTime, direction)
}
update (add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now()
const removed = this.removeBatch(remove, startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 1000)
if (resetLayout) {
add.forEach(tx => {
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
})
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
this.place(tx)
})
} else {
// try to insert new txs directly
const remaining = []
add.map(tx => new TxView(tx, this.vertexArray)).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
if (!this.tryInsertByFee(tx)) {
remaining.push(tx)
}
})
this.placeBatch(remaining)
this.layout.applyGravity()
}
this.updateAll(startTime, direction)
}
//return the tx at this screen position, if any
getTxAt (position: Position): TxView | void {
if (this.layout) {
const gridPosition = this.screenToGrid(position)
return this.layout.getTx(gridPosition)
} else return null
}
private init ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }): void {
this.vertexArray = vertexArray
this.scene = {
count: 0,
offset: {
x: 0,
y: 0
}
}
// Set the scale of the visualization (with a 5% margin)
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2)
this.gridWidth = resolution
this.gridHeight = resolution
this.resize({ width, height })
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
this.txs = {}
this.initialised = true
this.dirty = true
}
private insert (tx: TxView, startTime: number, direction: string = 'left'): void {
this.txs[tx.txid] = tx
this.place(tx)
this.updateTx(tx, startTime, direction)
}
private updateTx (tx: TxView, startTime: number, direction: string = 'left'): void {
if (tx.dirty || this.dirty) {
this.saveGridToScreenPosition(tx)
this.setTxOnScreen(tx, startTime, direction)
}
}
private setTxOnScreen (tx: TxView, startTime: number, direction: string = 'left'): void {
if (!tx.initialised) {
const txColor = tx.getColor()
tx.update({
display: {
position: {
x: tx.screenPosition.x + (direction == 'right' ? -this.width : this.width) * 1.4,
y: tx.screenPosition.y,
s: tx.screenPosition.s
},
color: txColor,
},
start: startTime,
delay: 0,
})
tx.update({
display: {
position: tx.screenPosition,
color: txColor
},
duration: 1000,
start: startTime,
delay: 50,
})
} else {
tx.update({
display: {
position: tx.screenPosition
},
duration: 1000,
minDuration: 500,
start: startTime,
delay: 50,
adjust: true
})
}
}
private updateAll (startTime: number, direction: string = 'left'): void {
this.scene.count = 0
const ids = this.getTxList()
startTime = startTime || performance.now()
for (let i = 0; i < ids.length; i++) {
this.updateTx(this.txs[ids[i]], startTime, direction)
}
this.dirty = false
}
private remove (id: string, startTime: number, direction: string = 'left'): TxView | void {
const tx = this.txs[id]
if (tx) {
this.layout.remove(tx)
tx.update({
display: {
position: {
x: tx.screenPosition.x + (direction == 'right' ? this.width : -this.width) * 1.4,
y: this.txs[id].screenPosition.y,
}
},
duration: 1000,
start: startTime,
delay: 50
})
}
delete this.txs[id]
return tx
}
private getTxList (): string[] {
return Object.keys(this.txs)
}
private saveGridToScreenPosition (tx: TxView): void {
tx.screenPosition = this.gridToScreen(tx.gridPosition)
}
// convert grid coordinates to screen coordinates
private gridToScreen (position: Square | void): Square {
if (position) {
const slotSize = (position.s * this.gridSize)
const squareSize = slotSize - (this.unitPadding * 2)
// The grid is laid out notionally left-to-right, bottom-to-top
// So we rotate 90deg counterclockwise then flip the y axis
//
// grid screen
// ________ ________ ________
// | | | b| | a|
// | | rotate | | flip | c |
// | c | --> | c | --> | |
// |a______b| |_______a| |_______b|
return {
x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
s: squareSize
}
} else {
return { x: 0, y: 0, s: 0 }
}
}
screenToGrid (position: Position): Position {
const grid = {
x: Math.floor((position.y - this.unitPadding) / this.gridSize),
y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
}
return grid
}
// calculates and returns the size of the tx in multiples of the grid size
private txSize (tx: TxView): number {
let scale = Math.max(1,Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)))
return Math.min(this.gridWidth, Math.max(1, scale)) // bound between 1 and the max displayable size (just in case!)
}
private place (tx: TxView): void {
const size = this.txSize(tx)
this.layout.insert(tx, size)
}
private tryInsertByFee (tx: TxView): boolean {
const size = this.txSize(tx)
const position = this.layout.tryInsertByFee(tx, size)
if (position) {
this.txs[tx.txid] = tx
return true
} else {
return false
}
}
// Add a list of transactions to the layout,
// keeping everything approximately sorted by feerate.
private placeBatch (txs: TxView[]): void {
if (txs.length) {
// grab the new tx with the highest fee rate
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
let i = 0
let maxSize = txs.reduce((max, tx) => {
return Math.max(this.txSize(tx), max)
}, 1) * 2
// find a reasonable place for it in the layout
const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize)
// extract a sub tree of transactions from the layout, rooted at that point
const popped = this.layout.popTree(root.x, root.y, maxSize)
// combine those with the new transactions and sort
txs = txs.concat(popped)
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
// insert everything back into the layout
txs.forEach(tx => {
this.txs[tx.txid] = tx
this.place(tx)
})
}
}
private removeBatch (ids: string[], startTime: number, direction: string = 'left'): TxView[] {
if (!startTime) startTime = performance.now()
return ids.map(id => {
return this.remove(id, startTime, direction)
}).filter(tx => tx != null) as TxView[]
}
}
class Slot {
l: number
r: number
w: number
constructor (l: number, r: number) {
this.l = l
this.r = r
this.w = r - l
}
intersects (slot: Slot): boolean {
return !((slot.r <= this.l) || (slot.l >= this.r))
}
subtract (slot: Slot): Slot[] | void {
if (this.intersects(slot)) {
// from middle
if (slot.l > this.l && slot.r < this.r) {
return [
new Slot(this.l, slot.l),
new Slot(slot.r, this.r)
]
} // totally covered
else if (slot.l <= this.l && slot.r >= this.r) {
return []
} // from left side
else if (slot.l <= this.l) {
if (slot.r == this.r) return []
else return [new Slot(slot.r, this.r)]
} // from right side
else if (slot.r >= this.r) {
if (slot.l == this.l) return []
else return [new Slot(this.l, slot.l)]
}
} else return [this]
}
}
class TxSlot extends Slot {
tx: TxView
constructor (l: number, r: number, tx: TxView) {
super(l, r)
this.tx = tx
}
}
class Row {
y: number
w: number
filled: TxSlot[]
slots: Slot[]
constructor (y: number, width: number) {
this.y = y
this.w = width
this.filled = []
this.slots = [new Slot(0, this.w)]
}
// insert a transaction w/ given width into row starting at position x
insert (x: number, w: number, tx: TxView): void {
const newSlot = new TxSlot(x, x + w, tx)
// insert into filled list
let index = this.filled.findIndex((slot) => { return slot.l >= newSlot.r })
if (index < 0) index = this.filled.length
this.filled.splice(index || 0, 0, newSlot)
// subtract from overlapping slots
for (let i = 0; i < this.slots.length; i++) {
if (newSlot.intersects(this.slots[i])) {
const diff = this.slots[i].subtract(newSlot)
if (diff) {
this.slots.splice(i, 1, ...diff)
i += diff.length - 1
}
}
}
}
remove (x: number, w: number): void {
const txIndex = this.filled.findIndex((slot) => { return slot.l == x })
this.filled.splice(txIndex, 1)
const newSlot = new Slot(x, x + w)
let slotIndex = this.slots.findIndex((slot) => { return slot.l >= newSlot.r })
if (slotIndex < 0) slotIndex = this.slots.length
this.slots.splice(slotIndex || 0, 0, newSlot)
this.normalize()
}
// merge any contiguous empty slots
private normalize (): void {
for (let i = 0; i < this.slots.length - 1; i++) {
if (this.slots[i].r == this.slots[i+1].l) {
this.slots[i].r = this.slots[i+1].r
this.slots[i].w += this.slots[i+1].w
this.slots.splice(i+1, 1)
i--
}
}
}
txAt (x: number): TxView | void {
let i = 0
while (i < this.filled.length && this.filled[i].l <= x) {
if (this.filled[i].l <= x && this.filled[i].r > x) return this.filled[i].tx
i++
}
}
getSlotsBetween (left: number, right: number): TxSlot[] {
const range = new Slot(left, right)
return this.filled.filter(slot => {
return slot.intersects(range)
})
}
slotAt (x: number): Slot | void {
let i = 0
while (i < this.slots.length && this.slots[i].l <= x) {
if (this.slots[i].l <= x && this.slots[i].r > x) return this.slots[i]
i++
}
}
getAvgFeerate (): number {
let count = 0
let total = 0
this.filled.forEach(slot => {
if (slot.tx) {
count += slot.w
total += (slot.tx.feerate * slot.w)
}
})
return total / count
}
}
class BlockLayout {
width: number;
height: number;
rows: Row[];
txPositions: { [key: string]: Square }
txs: { [key: string]: TxView }
constructor ({ width, height } : { width: number, height: number }) {
this.width = width
this.height = height
this.rows = [new Row(0, this.width)]
this.txPositions = {}
this.txs = {}
}
getRow (position: Square): Row {
return this.rows[position.y]
}
getTx (position: Square): TxView | void {
if (this.getRow(position)) {
return this.getRow(position).txAt(position.x)
}
}
addRow (): void {
this.rows.push(new Row(this.rows.length, this.width))
}
remove (tx: TxView) {
const position = this.txPositions[tx.txid]
if (position) {
for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
this.rows[y].remove(position.x, position.s)
}
}
delete this.txPositions[tx.txid]
delete this.txs[tx.txid]
}
insert (tx: TxView, width: number): Square {
const fit = this.fit(tx, width)
// insert the tx into rows at that position
for (let y = fit.y; y < fit.y + width; y++) {
if (y >= this.rows.length) this.addRow()
this.rows[y].insert(fit.x, width, tx)
}
const position = { x: fit.x, y: fit.y, s: width }
this.txPositions[tx.txid] = position
this.txs[tx.txid] = tx
tx.applyGridPosition(position)
return position
}
// Find the first slot large enough to hold a transaction of this size
fit (tx: TxView, width: number): Square {
let fit
for (let y = 0; y < this.rows.length && !fit; y++) {
fit = this.findFit(0, this.width, y, y, width)
}
// fall back to placing tx in a new row at the top of the layout
if (!fit) {
fit = { x: 0, y: this.rows.length }
}
return fit
}
// recursively check rows to see if there's space for a tx (depth-first)
// left/right: initial column boundaries to check
// row: current row to check
// start: starting row
// size: size of space needed
findFit (left: number, right: number, row: number, start: number, size: number) : Square {
if ((row - start) >= size || row >= this.rows.length) {
return { x: left, y: start }
}
for (let i = 0; i < this.rows[row].slots.length; i++) {
const slot = this.rows[row].slots[i]
const l = Math.max(left, slot.l)
const r = Math.min(right, slot.r)
if (r - l >= size) {
const fit = this.findFit(l, r, row + 1, start, size)
if (fit) return fit
}
}
}
// insert only if the tx fits into a fee-appropriate position
tryInsertByFee (tx: TxView, size: number): Square | void {
const fit = this.fit(tx, size)
if (this.checkRowFees(fit.y, tx.feerate)) {
// insert the tx into rows at that position
for (let y = fit.y; y < fit.y + size; y++) {
if (y >= this.rows.length) this.addRow()
this.rows[y].insert(fit.x, size, tx)
}
const position = { x: fit.x, y: fit.y, s: size }
this.txPositions[tx.txid] = position
this.txs[tx.txid] = tx
tx.applyGridPosition(position)
return position
}
}
// Return the first slot with a lower feerate
getReplacementRoot (feerate: number, width: number): Square {
let slot
for (let row = 0; row <= this.rows.length; row++) {
if (this.rows[row].slots.length > 0) {
return { x: this.rows[row].slots[0].l, y: row }
} else {
slot = this.rows[row].filled.find(x => {
return x.tx.feerate < feerate
})
if (slot) {
return { x: Math.min(slot.l, this.width - width), y: row }
}
}
}
return { x: 0, y: this.rows.length }
}
// remove and return all transactions in a subtree of the layout
popTree (x: number, y: number, width: number) {
const selected: { [key: string]: TxView } = {}
let left = x
let right = x + width
let prevWidth = right - left
let prevFee = Infinity
// scan rows upwards within a channel bounded by 'left' and 'right'
for (let row = y; row < this.rows.length; row++) {
let rowMax = 0
let slots = this.rows[row].getSlotsBetween(left, right)
// check each slot in this row overlapping the search channel
slots.forEach(slot => {
// select the associated transaction
selected[slot.tx.txid] = slot.tx
rowMax = Math.max(rowMax, slot.tx.feerate)
// widen the search channel to accommodate this slot if necessary
if (slot.w > prevWidth) {
left = slot.l
right = slot.r
// if this slot's tx has a higher feerate than the max in the previous row
// (i.e. it's out of position)
// select all txs overlapping the slot's full width in some rows *below*
// to free up space for this tx to sink down to its proper position
if (slot.tx.feerate > prevFee) {
let count = 0
// keep scanning back down until we find a full row of higher-feerate txs
for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
let echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r)
count = 0
echoSlots.forEach(echoSlot => {
selected[echoSlot.tx.txid] = echoSlot.tx
if (echoSlot.tx.feerate >= slot.tx.feerate) {
count += echoSlot.w
}
})
}
}
}
})
prevWidth = right - left
prevFee = rowMax
}
const txList = Object.values(selected)
txList.forEach(tx => {
this.remove(tx)
})
return txList
}
// Check if this row has high enough avg fees
// for a tx with this feerate to make sense here
checkRowFees (row: number, targetFee: number): boolean {
// first row is always fine
if (row == 0 || !this.rows[row]) return true
return (this.rows[row].getAvgFeerate() > (targetFee * 0.9))
}
// drop any free-floating transactions down into empty spaces
applyGravity (): void {
Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
return posA.y - posB.y || posA.x - posB.x
}).forEach(([txid, position]) => {
// see how far this transaction can fall
let dropTo = position.y
while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
dropTo--;
}
// if it can fall at all
if (dropTo < position.y) {
// remove and reinsert in the row we found
const tx = this.txs[txid]
this.remove(tx)
this.insert(tx, position.s)
}
})
}
}

View File

@@ -0,0 +1,105 @@
/*
Utility class for access and management of low-level sprite data
Maintains a single Float32Array of sprite data, keeping track of empty slots
to allow constant-time insertion and deletion
Automatically resizes by copying to a new, larger Float32Array when necessary,
or compacting into a smaller Float32Array when there's space to do so.
*/
import TxSprite from './tx-sprite';
export class FastVertexArray {
length: number;
count: number;
stride: number;
sprites: TxSprite[];
data: Float32Array;
freeSlots: number[];
lastSlot: number;
constructor(length, stride) {
this.length = length;
this.count = 0;
this.stride = stride;
this.sprites = [];
this.data = new Float32Array(this.length * this.stride);
this.freeSlots = [];
this.lastSlot = 0;
}
insert(sprite: TxSprite): number {
this.count++;
let position;
if (this.freeSlots.length) {
position = this.freeSlots.shift();
} else {
position = this.lastSlot;
this.lastSlot++;
if (this.lastSlot > this.length) {
this.expand();
}
}
this.sprites[position] = sprite;
return position;
}
remove(index: number): void {
this.count--;
this.clearData(index);
this.freeSlots.push(index);
this.sprites[index] = null;
if (this.length > 2048 && this.count < (this.length * 0.4)) {
this.compact();
}
}
setData(index: number, dataChunk: number[]): void {
this.data.set(dataChunk, (index * this.stride));
}
clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
}
getData(index: number): Float32Array {
return this.data.subarray(index, this.stride);
}
expand(): void {
this.length *= 2;
const newData = new Float32Array(this.length * this.stride);
newData.set(this.data);
this.data = newData;
}
compact(): void {
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength !== this.length) {
this.length = newLength;
this.data = new Float32Array(this.length * this.stride);
let sprite;
const newSprites = [];
let i = 0;
for (const index in this.sprites) {
sprite = this.sprites[index];
if (sprite) {
newSprites.push(sprite);
sprite.moveVertexPointer(i);
sprite.compile();
i++;
}
}
this.sprites = newSprites;
this.freeSlots = [];
this.lastSlot = i;
}
}
getVertexData(): Float32Array {
return this.data;
}
}

View File

@@ -0,0 +1,6 @@
<div class="mempool-block-overview">
<canvas class="block-overview" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="!(isLoading$ | async)">
<div class="spinner-border ml-3 loading" role="status"></div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
.mempool-block-overview {
position: relative;
width: 100%;
padding-bottom: 100%;
background: #181b2d;
display: flex;
justify-content: center;
align-items: center;
}
.block-overview {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
}
.loader-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 500ms 500ms;
&.hidden {
opacity: 0;
transition: opacity 500ms;
}
}

View File

@@ -0,0 +1,408 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
import { switchMap, filter } from 'rxjs/operators';
import { WebsocketService } from 'src/app/services/websocket.service';
import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
import TxView from './tx-view';
@Component({
selector: 'app-mempool-block-overview',
templateUrl: './mempool-block-overview.component.html',
styleUrls: ['./mempool-block-overview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
gl: WebGLRenderingContext;
animationFrameRequest: number;
displayWidth: number;
displayHeight: number;
shaderProgram: WebGLProgram;
vertexArray: FastVertexArray;
running: boolean;
scene: BlockScene;
hoverTx: TxView | void;
selectedTx: TxView | void;
lastBlockHeight: number;
blockIndex: number;
isLoading$ = new BehaviorSubject<boolean>(true);
blockSub: Subscription;
deltaSub: Subscription;
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
readonly ngZone: NgZone,
) {
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
}
ngOnInit(): void {
this.blockSub = merge(
of(true),
this.stateService.connectionState$.pipe(filter((state) => state === 2))
)
.pipe(switchMap(() => this.stateService.mempoolBlockTransactions$))
.subscribe((transactionsStripped) => {
this.replaceBlock(transactionsStripped);
});
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
this.updateBlock(delta);
});
}
ngAfterViewInit(): void {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas();
this.resizeCanvas();
}
ngOnChanges(changes): void {
if (changes.index) {
this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
}
}
ngOnDestroy(): void {
this.blockSub.unsubscribe();
this.deltaSub.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
}
clearBlock(direction): void {
if (this.scene) {
this.scene.exit(direction);
}
this.hoverTx = null;
this.selectedTx = null;
this.txPreviewEvent.emit(null);
}
replaceBlock(transactionsStripped: TransactionStripped[]): void {
if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
}
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
this.scene.enter(transactionsStripped, direction);
} else {
this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index;
this.isLoading$.next(false);
}
updateBlock(delta: MempoolBlockDelta): void {
if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
}
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
this.scene.exit(direction);
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
this.scene.enter(delta.added, direction);
} else {
this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index;
this.isLoading$.next(false);
}
initCanvas(): void {
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const shaderSet = [
{
type: this.gl.VERTEX_SHADER,
src: vertShaderSrc
},
{
type: this.gl.FRAGMENT_SHADER,
src: fragShaderSrc
}
];
this.shaderProgram = this.buildShaderProgram(shaderSet);
this.gl.useProgram(this.shaderProgram);
// Set up alpha blending
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
const glBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
this.gl.enableVertexAttribArray(attribs[key].pointer);
});
this.start();
}
handleContextLost(event): void {
event.preventDefault();
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
}
handleContextRestored(event): void {
this.initCanvas();
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth;
this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
}
}
compileShader(src, type): WebGLShader {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
console.log(this.gl.getShaderInfoLog(shader));
}
return shader;
}
buildShaderProgram(shaderInfo): WebGLProgram {
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
const shader = this.compileShader(desc.src, desc.type);
if (shader) {
this.gl.attachShader(program, shader);
}
});
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.log('Error linking shader program:');
console.log(this.gl.getProgramInfoLog(program));
}
return program;
}
start(): void {
this.running = true;
this.ngZone.runOutsideAngular(() => this.run());
}
run(now?: DOMHighResTimeStamp): void {
if (!now) {
now = performance.now();
}
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
this.gl.vertexAttribPointer(attribs[key].pointer,
attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
/* LOOP */
if (this.running) {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
}
this.animationFrameRequest = requestAnimationFrame(() => this.run());
}
}
@HostListener('click', ['$event'])
onClick(event) {
this.setPreviewTx(event.offsetX, event.offsetY, true);
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
}
@HostListener('pointerleave', ['$event'])
onPointerLeave(event) {
this.setPreviewTx(-1, -1, false);
}
setPreviewTx(x: number, y: number, clicked: boolean = false) {
if (this.scene && (!this.selectedTx || clicked)) {
const selected = this.scene.getTxAt({ x, y });
const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) {
if (currentPreview) {
currentPreview.setHover(false);
}
if (selected) {
selected.setHover(true);
this.txPreviewEvent.emit({
txid: selected.txid,
fee: selected.fee,
vsize: selected.vsize,
value: selected.value
});
if (clicked) {
this.selectedTx = selected;
} else {
this.hoverTx = selected;
}
} else {
if (clicked) {
this.selectedTx = null;
}
this.hoverTx = null;
this.txPreviewEvent.emit(null);
}
} else if (clicked) {
if (selected === this.selectedTx) {
this.hoverTx = this.selectedTx;
this.selectedTx = null;
} else {
this.selectedTx = selected;
}
}
}
}
}
// WebGL shader attributes
const attribs = {
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
};
// Calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => {
return total + (attrib.count * 4);
}, 0);
// Calculate vertex attribute offsets
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
const attrib = Object.values(attribs)[i];
attrib.offset = offset;
offset += (attrib.count * 4);
}
const vertShaderSrc = `
varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec2 offset;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 posR;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
attribute vec4 colA;
uniform vec2 screenSize;
uniform float now;
float smootherstep(float x) {
x = clamp(x, 0.0, 1.0);
float ix = 1.0 - x;
x = x * x;
return x / (x + ix * ix);
}
float interpolateAttribute(vec4 attr) {
float d = (now - attr.z) * attr.w;
float delta = smootherstep(d);
return mix(attr.x, attr.y, delta);
}
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
float radius = interpolateAttribute(posR);
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);
float green = interpolateAttribute(colG);
float blue = interpolateAttribute(colB);
float alpha = interpolateAttribute(colA);
vColor = vec4(red, green, blue, alpha);
}
`;
const fragShaderSrc = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
// premultiply alpha
gl_FragColor.rgb *= gl_FragColor.a;
}
`;

View File

@@ -0,0 +1,74 @@
export type Position = {
x: number,
y: number,
};
export type Square = Position & {
s?: number
};
export type Color = {
r: number,
g: number,
b: number,
a: number,
};
export type InterpolatedAttribute = {
a: number,
b: number,
t: number,
v: number,
d: number
};
export type Update = Position & { s: number } & Color;
export type Attributes = {
x: InterpolatedAttribute,
y: InterpolatedAttribute,
s: InterpolatedAttribute,
r: InterpolatedAttribute,
g: InterpolatedAttribute,
b: InterpolatedAttribute,
a: InterpolatedAttribute
};
export type OptionalAttributes = {
x?: InterpolatedAttribute,
y?: InterpolatedAttribute,
s?: InterpolatedAttribute,
r?: InterpolatedAttribute,
g?: InterpolatedAttribute,
b?: InterpolatedAttribute,
a?: InterpolatedAttribute
};
export type SpriteUpdateParams = {
x?: number,
y?: number,
s?: number,
r?: number,
g?: number,
b?: number,
a?: number
start?: DOMHighResTimeStamp,
duration?: number,
minDuration?: number,
adjust?: boolean,
temp?: boolean
};
export type ViewUpdateParams = {
display: {
position?: Square,
color?: Color,
},
start?: number,
duration?: number,
minDuration?: number,
delay?: number,
jitter?: number,
state?: string,
adjust?: boolean
};

View File

@@ -0,0 +1,215 @@
import { FastVertexArray } from './fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
export default class TxSprite {
static vertexSize = 30;
static vertexCount = 6;
static dataSize: number = (30 * 6);
vertexArray: FastVertexArray;
vertexPointer: number;
vertexData: number[];
updateMap: Update;
attributes: Attributes;
tempAttributes: OptionalAttributes;
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
const offsetTime = params.start;
this.vertexArray = vertexArray;
this.vertexData = Array(VI.length).fill(0);
this.updateMap = {
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
};
this.attributes = {
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
s: { a: params.s, b: params.s, t: offsetTime, v: 0, d: 0 },
r: { a: params.r, b: params.r, t: offsetTime, v: 0, d: 0 },
g: { a: params.g, b: params.g, t: offsetTime, v: 0, d: 0 },
b: { a: params.b, b: params.b, t: offsetTime, v: 0, d: 0 },
a: { a: params.a, b: params.a, t: offsetTime, v: 0, d: 0 },
};
// Used to temporarily modify the sprite, so that the base view can be resumed later
this.tempAttributes = null;
this.vertexPointer = this.vertexArray.insert(this);
this.compile();
}
private interpolateAttributes(updateMap: Update, attributes: OptionalAttributes, offsetTime: DOMHighResTimeStamp, v: number,
duration: number, minDuration: number, adjust: boolean): void {
for (const key of Object.keys(updateMap)) {
// for each non-null attribute:
if (updateMap[key] != null) {
// calculate current interpolated value, and set as 'from'
interpolateAttributeStart(attributes[key], offsetTime);
// update start time
attributes[key].t = offsetTime;
if (!adjust || (duration && attributes[key].d === 0)) {
attributes[key].v = v;
attributes[key].d = duration;
} else if (minDuration > attributes[key].d) {
// enforce minimum transition duration
attributes[key].v = 1 / minDuration;
attributes[key].d = minDuration;
}
// set 'to' to target value
attributes[key].b = updateMap[key];
}
}
}
/*
params:
x, y, s: position & size of the sprite
r, g, b, a: color & opacity
start: performance.now() timestamp, when to start the transition
duration: of the tweening animation
adjust: if true, alter the target value of any conflicting transitions without changing the duration
minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume')
*/
update(params: SpriteUpdateParams): void {
const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0;
updateKeys.forEach(key => {
this.updateMap[key] = params[key];
});
const isModified = !!this.tempAttributes;
if (!params.temp) {
this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
} else {
if (!isModified) { // set up tempAttributes
this.tempAttributes = {};
for (const key of Object.keys(this.updateMap)) {
if (this.updateMap[key] != null) {
this.tempAttributes[key] = { ...this.attributes[key] };
}
}
}
this.interpolateAttributes(this.updateMap, this.tempAttributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
}
this.compile();
}
// Transition back from modified state back to base attributes
resume(duration: number, start: DOMHighResTimeStamp = performance.now()): void {
// If not in modified state, there's nothing to do
if (!this.tempAttributes) {
return;
}
const offsetTime = start;
const v = duration > 0 ? (1 / duration) : 0;
for (const key of Object.keys(this.tempAttributes)) {
// If this base attribute is static (fixed or post-transition), transition smoothly back
if (this.attributes[key].v === 0 || (this.attributes[key].t + this.attributes[key].d) <= start) {
// calculate current interpolated value, and set as 'from'
interpolateAttributeStart(this.tempAttributes[key], offsetTime);
this.attributes[key].a = this.tempAttributes[key].a;
this.attributes[key].t = offsetTime;
this.attributes[key].v = v;
this.attributes[key].d = duration;
}
}
this.tempAttributes = null;
this.compile();
}
// Write current state into the graphics vertex array for rendering
compile(): void {
let attributes = this.attributes;
if (this.tempAttributes) {
attributes = {
...this.attributes,
...this.tempAttributes
};
}
const size = attributes.s;
// update vertex data in place
// ugly, but avoids overhead of allocating large temporary arrays
const vertexStride = VI.length + 2;
for (let vertex = 0; vertex < 6; vertex++) {
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
for (let step = 0; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
}
}
this.vertexArray.setData(this.vertexPointer, this.vertexData);
}
moveVertexPointer(index: number): void {
this.vertexPointer = index;
}
destroy(): void {
this.vertexArray.remove(this.vertexPointer);
this.vertexPointer = null;
}
}
// expects 0 <= x <= 1
function smootherstep(x: number): number {
const ix = 1 - x;
x = x * x;
return x / (x + ix * ix);
}
function interpolateAttributeStart(attribute: InterpolatedAttribute, start: DOMHighResTimeStamp): void {
if (attribute.v === 0 || (attribute.t + attribute.d) <= start) {
// transition finished, next transition starts from current end state
// (clamp to 1)
attribute.a = attribute.b;
attribute.v = 0;
attribute.d = 0;
} else if (attribute.t > start) {
// transition not started
// (clamp to 0)
} else {
// transition in progress
// (interpolate)
const progress = (start - attribute.t);
const delta = smootherstep(progress / attribute.d);
attribute.a = attribute.a + (delta * (attribute.b - attribute.a));
attribute.d = attribute.d - progress;
attribute.v = 1 / attribute.d;
}
}
const vertexOffsetFactors = [
[0, 0],
[1, 1],
[1, 0],
[0, 0],
[1, 1],
[0, 1]
];
const VI = [];
updateKeys.forEach((attribute, aIndex) => {
attribKeys.forEach(field => {
VI.push({
a: attribute,
f: field
});
});
});

View File

@@ -0,0 +1,150 @@
import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
start: (params.start || performance.now()) + (params.delay || 0),
duration: params.duration,
minDuration: params.minDuration,
...params.display.position,
...params.display.color,
adjust: params.adjust
};
}
export default class TxView implements TransactionStripped {
txid: string;
fee: number;
vsize: number;
value: number;
feerate: number;
initialised: boolean;
vertexArray: FastVertexArray;
hover: boolean;
sprite: TxSprite;
hoverColor: Color | void;
screenPosition: Square;
gridPosition: Square | void;
dirty: boolean;
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
this.txid = tx.txid;
this.fee = tx.fee;
this.vsize = tx.vsize;
this.value = tx.value;
this.feerate = tx.fee / tx.vsize;
this.initialised = false;
this.vertexArray = vertexArray;
this.hover = false;
this.screenPosition = { x: 0, y: 0, s: 0 };
this.dirty = true;
}
destroy(): void {
if (this.sprite) {
this.sprite.destroy();
this.sprite = null;
this.initialised = false;
}
}
applyGridPosition(position: Square): void {
if (!this.gridPosition) {
this.gridPosition = { x: 0, y: 0, s: 0 };
}
if (this.gridPosition.x !== position.x || this.gridPosition.y !== position.y || this.gridPosition.s !== position.s) {
this.gridPosition.x = position.x;
this.gridPosition.y = position.y;
this.gridPosition.s = position.s;
this.dirty = true;
}
}
/*
display: defines the final appearance of the sprite
position: { x, y, s } (coordinates & size)
color: { r, g, b, a} (color channels & alpha)
duration: of the tweening animation from the previous display state
start: performance.now() timestamp, when to start the transition
delay: additional milliseconds to wait before starting
jitter: if set, adds a random amount to the delay,
adjust: if true, modify an in-progress transition instead of replacing it
*/
update(params: ViewUpdateParams): void {
if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
if (!this.initialised || !this.sprite) {
this.initialised = true;
this.sprite = new TxSprite(
toSpriteUpdate(params),
this.vertexArray
);
// apply any pending hover event
if (this.hover) {
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
}
} else {
this.sprite.update(
toSpriteUpdate(params)
);
}
this.dirty = false;
}
// Temporarily override the tx color
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void {
if (hoverOn) {
this.hover = true;
this.hoverColor = color;
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
} else {
this.hover = false;
this.hoverColor = null;
if (this.sprite) {
this.sprite.resume(hoverTransitionTime);
}
}
this.dirty = false;
}
getColor(): Color {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => (this.feerate || 1) >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
return hexToColor(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
}
}
function hexToColor(hex: string): Color {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1
};
}

View File

@@ -10,38 +10,69 @@
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<table class="table table-borderless table-striped table-fixed">
<tbody>
<tr>
<td i18n="mempool-block.median-fee">Median fee</td>
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>{{ mempoolBlock.nTx }}</td>
</tr>
<tr>
<td i18n="mempool-block.size">Size</td>
<td>
<div class="progress">
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
</div>
</td>
</tr>
<ng-container *ngIf="!previewTx">
<tr>
<td i18n="mempool-block.median-fee">Median fee</td>
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>{{ mempoolBlock.nTx }}</td>
</tr>
<tr>
<td i18n="mempool-block.size">Size</td>
<td>
<div class="progress">
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
</div>
</td>
</tr>
</ng-container>
<ng-container *ngIf="previewTx">
<tr>
<td i18n="shared.transaction">Transaction</td>
<td>
<a [routerLink]="['/tx/' | relativeUrl, previewTx.txid]">{{ previewTx.txid | shortenString : 16}}</a>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.value|Transaction value">Value</td>
<td><app-amount [satoshis]="previewTx.value"></app-amount></td>
</tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ previewTx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="previewTx.fee"></app-fiat></span></td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
{{ (previewTx.fee / previewTx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td>
</tr>
<tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (previewTx.vsize | vbytes: 2)"></td>
</tr>
</ng-container>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
<div class="col-md chart-container">
<app-fee-distribution-graph [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/
import { StateService } from 'src/app/services/state.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
import { MempoolBlock, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from 'src/app/services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@@ -18,13 +18,17 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
mempoolBlockIndex: number;
mempoolBlock$: Observable<MempoolBlock>;
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
previewTx: TransactionStripped | void;
webGlEnabled: boolean;
constructor(
private route: ActivatedRoute,
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
) { }
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
@@ -74,5 +78,15 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
} else {
return $localize`:@@mempool-block.block.no:Mempool block ${this.mempoolBlockIndex + 1}:INTERPOLATION:`;
}
}
}
setTxPreview(event: TransactionStripped | void): void {
this.previewTx = event
}
}
function detectWebGL () {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return (gl && gl instanceof WebGLRenderingContext)
}

View File

@@ -238,7 +238,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
gradientColors.push(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
});
gradientColors.forEach((color, i, gc) => {
backgroundGradients.push(`
#${i === 0 ? color : gc[i - 1]} ${ i === 0 ? emptyBackgroundSpacePercentage : ((i / gradientColors.length) * 100) * usedBlockSpace / 100 + emptyBackgroundSpacePercentage }%,

View File

@@ -64,7 +64,7 @@
}
.more-padding {
padding: 18px;
padding: 24px 20px !important;
}
.card-wrapper {

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
import { concat, Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from '../..//services/storage.service';
@@ -58,15 +58,7 @@ export class PoolRankingComponent implements OnInit {
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
// When...
this.miningStatsObservable$ = combineLatest([
// ...a new block is mined
this.stateService.blocks$
.pipe(
// (we always receives some blocks at start so only trigger for the last one)
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
),
// ...or we change the timespan
this.miningStatsObservable$ = concat(
this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.miningWindowPreference), // (trigger when the page loads)
@@ -76,18 +68,19 @@ export class PoolRankingComponent implements OnInit {
this.storageService.setValue('miningWindowPreference', value);
}
this.miningWindowPreference = value;
}),
switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference);
})
)
])
// ...then refresh the mining stats
),
this.stateService.blocks$
.pipe(
switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference);
})
)
)
.pipe(
switchMap(() => {
this.isLoading = true;
return this.miningService.getMiningStats(this.miningWindowPreference)
.pipe(
catchError((e) => of(this.getEmptyMiningStat()))
);
}),
map(data => {
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w

View File

@@ -1,6 +1,6 @@
<div class="container-xl">
<img [src]="'./resources/mempool-space-logo.png'" height="35" width="140" class="logo">
<app-svg-images name="officialMempoolSpace" class="logo" style="width: 140px; height: 35px; margin: 30px;" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div class="sponsor-page">

View File

@@ -124,10 +124,6 @@
}
}
.logo {
margin: 30px;
}
.card-body {
align-items: center;
display: flex;

View File

@@ -0,0 +1,28 @@
<ng-container [ngSwitch]="name">
<ng-container *ngSwitchCase="'officialMempoolSpace'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>
<path d="M189.767 113.263C183.867 113.263 177.803 111.392 174.637 108.617L174 108.062L177.104 102.594L178.05 103.334C180.434 105.205 185.08 107.096 190.054 107.096C195.79 107.096 198.565 105.554 198.565 102.368C198.565 99.3461 195.194 98.5856 189.664 97.6194C183.312 96.5094 175.398 95.1115 175.398 86.6012C175.398 79.8999 181.359 75.5625 190.589 75.5625C195.584 75.5625 200.785 76.9192 203.807 79.0365L204.567 79.571L201.484 85.2034L200.559 84.5867C197.804 82.7572 194.269 81.7499 190.609 81.7499C185.368 81.7499 182.366 83.4767 182.366 86.4779C182.366 89.7052 185.84 90.548 191.576 91.5759C197.784 92.6859 205.513 94.0632 205.513 102.224C205.513 105.575 204.012 108.37 201.175 110.323C198.421 112.255 194.474 113.263 189.767 113.263Z" fill="#9857FF"/>
<path d="M212.316 125.863V75.9733H218.997V80.8451C222.122 77.4945 226.541 75.7061 231.722 75.7061C236.984 75.7061 241.774 77.5356 245.186 80.8862C248.66 84.278 250.572 89.0882 250.572 94.4123C250.572 99.7569 248.66 104.588 245.206 108C241.774 111.392 237.005 113.242 231.742 113.242C226.624 113.242 222.327 111.536 219.162 108.247V125.843H212.316V125.863ZM231.434 81.8729C228.001 81.8729 224.876 83.1063 222.656 85.3264C220.354 87.6287 219.141 90.7738 219.141 94.4123C219.141 101.792 224.198 106.952 231.454 106.952C238.69 106.952 243.768 101.792 243.768 94.4123C243.768 90.7738 242.555 87.6287 240.252 85.3264C237.991 83.1063 234.867 81.8729 231.434 81.8729Z" fill="#9857FF"/>
<path d="M269.194 113.262C260.643 113.262 255.113 108.884 255.113 102.1C255.113 98.8936 256.285 96.2212 258.526 94.3712C261.054 92.2744 264.878 91.2055 269.893 91.2055H280.233V90.1777C280.233 84.7714 277.17 82.0168 271.147 82.0168C266.995 82.0168 262.781 83.4558 259.882 85.8814L258.916 86.7037L255.956 81.1329L256.593 80.5985C260.417 77.4328 265.823 75.7061 271.846 75.7061C281.734 75.7061 287.181 80.8862 287.181 90.301V112.975H280.5V109.398C277.89 111.906 273.984 113.262 269.194 113.262ZM270.181 96.6735C262.925 96.6735 262.082 99.613 262.082 101.813C262.082 105.348 264.98 107.363 270.037 107.363H270.058C270.181 107.363 270.304 107.363 270.448 107.363C275.115 107.363 278.547 105.225 280.377 101.175V96.6735H270.181V96.6735Z" fill="#9857FF"/>
<path d="M313.658 112.995C302.742 112.995 294.828 105.184 294.828 94.4123C294.828 89.1088 296.781 84.3191 300.337 80.9274C303.852 77.5561 308.786 75.7061 314.233 75.7061C320.77 75.7061 326.053 78.2961 329.137 83.0241L329.712 83.8875L324.347 87.3204L323.792 86.4776C321.777 83.4558 318.303 81.729 314.254 81.729C310.656 81.729 307.552 82.9213 305.291 85.1825C303.01 87.4643 301.797 90.6094 301.797 94.2684C301.797 97.9685 303.01 101.134 305.332 103.395C307.573 105.595 310.739 106.808 314.233 106.808C318.303 106.808 321.695 105.163 323.771 102.183L324.326 101.381L329.671 104.793L329.137 105.657C326.238 110.323 320.811 112.995 314.254 112.995C314.048 112.995 313.863 112.995 313.658 112.995Z" fill="#9857FF"/>
<path d="M352.118 113.386C346.506 113.386 341.429 111.516 337.852 108.103C334.255 104.691 332.281 99.881 332.281 94.5363C332.281 83.7031 340.031 75.8301 350.7 75.8301C361.204 75.8301 368.83 83.5387 368.995 94.228C368.995 94.8447 368.995 96.9414 368.995 96.9414H339.373C340.36 103.047 345.396 107.076 352.241 107.076C356.414 107.076 360.032 105.596 362.417 102.903L363.177 102.039L367.206 106.603L366.61 107.281C363.218 111.228 358.059 113.386 352.118 113.386ZM362.335 91.3295C361.368 85.6354 356.805 81.7297 350.864 81.7297C344.964 81.7297 340.483 85.5327 339.414 91.3295H362.335Z" fill="#9857FF"/>
<path d="M219.548 56.1979V33.8327C219.548 30.3587 218.746 27.6863 217.163 25.9185C215.601 24.1507 213.237 23.2667 210.195 23.2667C206.762 23.2667 203.946 24.3768 202.013 26.453C200.081 28.5497 199.053 31.6331 199.053 35.395V56.2185H191.447V33.8327C191.447 26.823 188.282 23.2667 182.032 23.2667C178.6 23.2667 175.783 24.3768 173.851 26.453C171.919 28.5497 170.891 31.6331 170.891 35.395V56.2185H163.285V16.4215H170.685V20.7589C173.687 17.7988 178.003 16.1748 182.999 16.1748C188.96 16.1748 193.667 18.4977 196.36 22.7528C199.608 18.5593 204.85 16.1748 210.955 16.1748C215.93 16.1748 219.877 17.6137 222.693 20.43C225.632 23.3901 227.174 27.8713 227.154 33.3599V56.1979H219.548V56.1979Z" fill="white"/>
<path d="M255.397 56.5679C243.145 56.5679 234.594 48.2426 234.594 36.32C234.594 24.644 242.775 16.1748 254.04 16.1748C265.387 16.1748 273.342 24.459 273.383 36.32C273.383 36.4433 273.363 36.5872 273.363 36.7106C273.342 36.8339 273.342 36.9367 273.342 37.0395V37.1011L273.322 37.1628C273.281 37.3684 273.281 37.5945 273.281 37.8617V39.1568H242.343C243.412 45.4881 248.49 49.5377 255.5 49.5377C259.796 49.5377 263.393 47.996 265.901 45.0975L266.888 44.008L271.41 49.1677L270.732 50.0516C267.196 54.2451 261.749 56.5679 255.397 56.5679ZM265.798 32.846C265.284 30.0503 263.989 27.6658 262.036 25.9185C259.919 24.0273 257.144 23.0406 254.061 23.0406C250.998 23.0406 248.264 24.0273 246.146 25.9185C244.194 27.6658 242.919 30.0503 242.405 32.846H265.798Z" fill="white"/>
<path d="M337.274 56.198V33.8327C337.274 30.3587 336.473 27.6864 334.89 25.9185C333.328 24.1507 330.964 23.2668 327.921 23.2668C324.488 23.2668 321.672 24.3768 319.74 26.453C317.808 28.5497 316.78 31.6332 316.78 35.395V56.2185H309.174V33.8327C309.174 26.823 306.008 23.2668 299.759 23.2668C296.326 23.2668 293.51 24.3768 291.578 26.453C289.645 28.5497 288.618 31.6332 288.618 35.395V56.2185H281.012V16.4215H288.412V20.7589C291.413 17.7782 295.73 16.1543 300.725 16.1543C306.687 16.1543 311.394 18.4772 314.087 22.7323C317.335 18.5388 322.577 16.1543 328.682 16.1543C333.656 16.1543 337.603 17.5932 340.419 20.4095C343.359 23.3696 344.901 27.8508 344.88 33.3394V56.1774H337.274V56.198Z" fill="white"/>
<path d="M354.297 69.9296V16.4215H361.697V21.3139C364.986 17.9427 369.467 16.1543 374.75 16.1543C386.241 16.1543 394.567 24.6235 394.567 36.2995C394.567 48.0371 386.241 56.5474 374.75 56.5474C369.57 56.5474 365.171 54.8413 361.923 51.5728V69.909H354.297V69.9296ZM374.422 23.1023C367.021 23.1023 361.841 28.5292 361.841 36.32C361.841 44.1109 367.021 49.5377 374.422 49.5377C381.822 49.5377 387.002 44.1109 387.002 36.32C387.002 28.6525 381.719 23.1023 374.422 23.1023Z" fill="white"/>
<path d="M420.321 56.5679C408.604 56.5679 400.094 48.0576 400.094 36.32C400.094 24.644 408.604 16.1748 420.321 16.1748C425.995 16.1748 431.134 18.2099 434.813 21.91C438.472 25.5896 440.466 30.7081 440.446 36.3405C440.446 48.0576 431.977 56.5679 420.321 56.5679ZM420.321 23.1023C412.921 23.1023 407.741 28.5291 407.741 36.32C407.741 44.1108 412.921 49.5377 420.321 49.5377C427.66 49.5377 432.799 44.1108 432.799 36.32C432.778 28.5291 427.66 23.1023 420.321 23.1023Z" fill="white"/>
<path d="M464.598 56.5679C452.881 56.5679 444.371 48.0576 444.371 36.32C444.371 24.644 452.881 16.1748 464.598 16.1748C476.254 16.1748 484.723 24.644 484.723 36.32C484.723 48.0576 476.254 56.5679 464.598 56.5679ZM464.598 23.1023C457.198 23.1023 452.018 28.5291 452.018 36.32C452.018 44.1108 457.198 49.5377 464.598 49.5377C471.937 49.5377 477.076 44.1108 477.076 36.32C477.076 28.5291 471.958 23.1023 464.598 23.1023Z" fill="white"/>
<path d="M499.996 1.14844H492.391V56.1982H499.996V1.14844Z" fill="white"/>
<path d="M124.706 110.25C124.706 118.849 117.772 125.791 109.183 125.791H15.5236C6.93387 125.791 0 118.849 0 110.25V16.4837C0 7.88416 6.98561 0.942383 15.5236 0.942383H109.183C117.772 0.942383 124.706 7.88416 124.706 16.4837V110.25Z" fill="#2E3349"/>
<path d="M0 63.5225V110.25C0 118.849 6.98561 125.791 15.5753 125.791H109.183C117.772 125.791 124.758 118.849 124.758 110.25V63.5225H0Z" [attr.fill]="'url(#paint0_linear' + randomId + ')'"/>
<path opacity="0.3" d="M109.909 109.11C109.909 111.026 108.615 112.581 107.011 112.581H90.8665C89.2624 112.581 87.9688 111.026 87.9688 109.11V17.6232C87.9688 15.7065 89.2624 14.1523 90.8665 14.1523H107.011C108.615 14.1523 109.909 15.7065 109.909 17.6232V109.11Z" fill="white"/>
<defs>
<linearGradient [id]="'paint0_linear' + randomId" x1="62.3768" y1="36.3949" x2="62.3768" y2="156.837" gradientUnits="userSpaceOnUse">
<stop stop-color="#AE61FF"/>
<stop offset="1" stop-color="#13EFD8"/>
</linearGradient>
</defs>
</svg>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-svg-images',
templateUrl: './svg-images.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SvgImagesComponent {
randomId = Math.floor(Math.random() * 10000);
@Input() name: string;
@Input() class: string;
@Input() style: string;
@Input() width: string;
@Input() height: string;
@Input() viewBox: string;
}

View File

@@ -66,13 +66,16 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<a *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<ng-template #vinScriptPubkeyType>
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
<div>
<app-address-labels [vin]="vin"></app-address-labels>
</div>

View File

@@ -5,7 +5,7 @@
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body">
<div class="card-body less-padding">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div>
@@ -33,7 +33,7 @@
<div class="col card-wrapper">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body">
<div class="card-body less-padding">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div>

View File

@@ -269,6 +269,9 @@
justify-content: space-around;
padding: 22px 20px;
}
.less-padding {
padding: 20px 20px;
}
}
.retarget-sign {

View File

@@ -2672,48 +2672,216 @@ export const restApiDocsData = [
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 10 blocks. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
},
urlString: "/blocks[/:startHeight]",
showConditions: bitcoinNetworks.concat(liquidNetworks).concat(["bisq"]),
urlString: "/v1/blocks[/:startHeight]",
showConditions: bitcoinNetworks,
showJsExamples: showJsExamplesDefault,
codeExample: {
bisq: {
default: {
codeTemplate: {
curl: `/api/blocks/%{1}/%{2}`,
curl: `/api/v1/blocks/%{1}`,
commonJS: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
`,
esModule: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
console.log(getBlocks);
`,
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: emptyCodeSample,
codeSampleBisq: {
esModule: ['0', '1'],
commonJS: ['0', '1'],
curl: ['0', '1'],
codeSampleMainnet: {
esModule: ['730000'],
commonJS: ['730000'],
curl: ['730000'],
response: `[
{
height: 698771,
time: 1630636953000,
hash: "0000000000000000000a33c6ac863eee8c76ca72435f25d679609c0949ac9374",
previousBlockHash: "00000000000000000001e4184639e5600d3fb4c4e06c2a625e76804c4bc93cb1",
txs: []
}
"id": "0000000000000000000384f28cb3b9cf4377a39cfd6c29ae9466951de38c0529",
"timestamp": 1648829449,
"height": 730000,
"version": 536870912,
"bits": 386521239,
"nonce": 3580664066,
"difficulty": 28587155782195.14,
"merkle_root": "efa344bcd6c0607f93b709515dd6dc5496178112d680338ebea459e3de7b4fbc",
"tx_count": 1627,
"size": 1210916,
"weight": 3993515,
"previousblockhash": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
"extras": {
"coinbaseRaw": "0390230b1362696e616e63652f383038e0006f02cd583765fabe6d6d686355577affaad03015e732428a927a5d2d842471b350394139616bcb4401d804000000000000001a750000c9ad0000",
"medianFee": 11,
"feeRange": [
1,
11,
11,
11,
18,
21,
660
],
"reward": 641321983,
"totalFees": 16321983,
"avgFee": 10038,
"avgFeeRate": 16,
"pool": {
"id": 105,
"name": "Binance Pool",
"slug": "binancepool"
}
}
},
{
"id": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
"timestamp": 1648828946,
"height": 729999,
"version": 793796608,
"bits": 386521239,
"nonce": 3477019455,
"difficulty": 28587155782195.14,
"merkle_root": "d84f9cc1823bd069c505061b1f6faabd809d67ab5354e9f6234312dc4bdb1ecf",
"tx_count": 2574,
"size": 1481957,
"weight": 3993485,
"previousblockhash": "000000000000000000071e6c86c2175aa86817cae2a77acd95372b55c1103d89",
"extras": {
"coinbaseRaw": "038f230b1362696e616e63652f373739d8002900ca5de7a9fabe6d6dda31112c36c10a523154eae76847579755cd4ae558ee2e6f9f200b05dd32a0bf04000000000000006372000000020000",
"medianFee": 17,
"feeRange": [
2,
11,
14,
17,
19,
28,
502
],
"reward": 649090210,
"totalFees": 24090210,
"avgFee": 9362,
"avgFeeRate": 24,
"pool": {
"id": 105,
"name": "Binance Pool",
"slug": "binancepool"
}
}
},
...
]`,
},
codeSampleTestnet: {
esModule: ['2091187'],
commonJS: ['2091187'],
curl: ['2091187'],
response: `[
{
"id": "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
"timestamp": 1630641890,
"height": 2091187,
"version": 545259520,
"bits": 436273151,
"nonce": 309403673,
"difficulty": 16777216,
"merkle_root": "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
"tx_count": 26,
"size": 8390,
"weight": 22985,
"previousblockhash": "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
"extras": {
"coinbaseRaw": "03b3e81f3a205468697320626c6f636b20776173206d696e65642077697468206120636172626f6e206e6567617469766520706f77657220736f75726365201209687a2009092009020de601d7986a040000",
"medianFee": 1,
"feeRange": [
1,
1,
1,
1,
5,
56,
5053
],
"reward": 10547567,
"totalFees": 781942,
"avgFee": 31277,
"avgFeeRate": 143,
"pool": {
"id": 137,
"name": "Unknown",
"slug": "unknown"
}
}
},
...
]`
},
},
codeSampleSignet: {
esModule: ['53783'],
commonJS: ['53783'],
curl: ['53783'],
response: `[
{
"id": "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
"timestamp": 1630641504,
"height": 53783,
"version": 536870912,
"bits": 503404179,
"nonce": 11753379,
"difficulty": 0.002919030932507782,
"merkle_root": "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
"tx_count": 1,
"size": 343,
"weight": 1264,
"previousblockhash": "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
"extras": {
"coinbaseRaw": "0317d200",
"medianFee": 0,
"feeRange": [
0,
0,
0,
0,
0,
0,
0
],
"reward": 5000000000,
"totalFees": 0,
"avgFee": 0,
"avgFeeRate": 0,
"pool": {
"id": 137,
"name": "Unknown",
"slug": "unknown"
}
}
},
...
]`
},
codeSampleLiquid: emptyCodeSample,
codeSampleLiquidTestnet: emptyCodeSample,
codeSampleBisq: emptyCodeSample,
}
}
},
{
type: "endpoint",
category: "blocks",
httpRequestMethod: "GET",
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 10 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
},
urlString: "/blocks[/:startHeight]",
showConditions: liquidNetworks,
showJsExamples: showJsExamplesDefault,
codeExample: {
default: {
codeTemplate: {
curl: `/api/blocks/%{1}`,
@@ -2731,75 +2899,9 @@ export const restApiDocsData = [
console.log(getBlocks);
`,
},
codeSampleMainnet: {
esModule: ['698777'],
commonJS: ['698777'],
curl: ['698777'],
response: `[
{
id: "00000000000000000003002915e015c47610c55b6f0228ad62bfcc59b65e67b7",
height: 698777,
version: 536870916,
timestamp: 1630641711,
tx_count: 2327,
size: 1466537,
weight: 3999653,
merkle_root: "023e27dde144eedc65ff3b27c535ebc7dced6c49fe78f94cdf85cf2000608d2f",
previousblockhash: "0000000000000000000701a7f14e362d3f10aa524200db1710ce3bbf0c0f8b75",
mediantime: 1630636986,
nonce: 1926094388,
bits: 386923168,
difficulty: 17615033039278
},
...
]`
},
codeSampleTestnet: {
esModule: ['2091187'],
commonJS: ['2091187'],
curl: ['2091187'],
response: `[
{
id: "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
height: 2091187,
version: 545259520,
timestamp: 1630641890,
tx_count: 26,
size: 8390,
weight: 22985,
merkle_root: "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
previousblockhash: "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
mediantime: 1630639627,
nonce: 309403673,
bits: 436273151,
difficulty: 16777216
},
...
]`
},
codeSampleSignet: {
esModule: ['53783'],
commonJS: ['53783'],
curl: ['53783'],
response: `[
{
id: "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
height: 53783,
version: 536870912,
timestamp: 1630641504,
tx_count: 1,
size: 343,
weight: 1264,
merkle_root: "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
previousblockhash: "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
mediantime: 1630638966,
nonce: 11753379,
bits: 503404179,
difficulty: 0
},
...
]`
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: {
esModule: ['1472246'],
commonJS: ['1472246'],
@@ -2848,144 +2950,79 @@ export const restApiDocsData = [
type: "endpoint",
category: "blocks",
httpRequestMethod: "GET",
fragment: "get-blocks-extras",
title: "GET Blocks Extras",
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
default: "<p>Returns the past <code>n</code> blocks with BSQ transactions starting <code>m</code> blocks ago.</p><p>Assume a block height of 700,000. Query <code>/blocks/0/10</code> for the past 10 blocks before 700,000 with BSQ transactions. Query <code>/blocks/1000/10</code> for the past 10 blocks before 699,000 with BSQ transactions."
},
urlString: "/blocks-extras[/:startHeight]",
showConditions: bitcoinNetworks,
showJsExamples: showJsExamplesDefaultFalse,
urlString: "/blocks/:m/:n",
showConditions: ["bisq"],
showJsExamples: showJsExamplesDefault,
codeExample: {
default: {
codeTemplate: {
curl: `/api/blocks-extras/%{1}`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ['736915'],
response: `[
{
"extras": {
"reward": 629766074,
"coinbaseTx": {
"vin": [
{
"scriptsig": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000"
}
],
"vout": [
{
"scriptpubkey_address": "1A32KFEX7JNPmU1PVjrtiXRrTQcesT3Nf1",
"value": 629766074
}
]
},
"coinbaseRaw": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000",
"medianFee": 14,
"feeRange": [
1,
1,
4,
14,
15,
20,
364
],
"totalFees": 4766074,
"avgFee": 5043,
"avgFeeRate": 14,
"pool": {
"id": 115,
"name": "MARA Pool",
"slug": "marapool"
},
"matchRate": 100
},
"id": "00000000000000000000a742ae476dbe2a58c48b193484945c52b05967f2d74c",
"height": 736915,
"version": 541065216,
"timestamp": 1652877171,
"bits": 386466234,
"nonce": 4069175824,
"difficulty": 31251101365711.12,
"merkle_root": "de54fd1adee9f010534e8efbf1244a01528e20dd283c8927026f5442c3e03459",
"tx_count": 946,
"size": 524907,
"weight": 1362339,
"previousblockhash": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe"
},
{
"extras": {
"reward": 638804415,
"coinbaseTx": {
"vin": [
{
"scriptsig": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f"
}
],
"vout": [
{
"scriptpubkey_address": "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE",
"value": 638804415
}
]
},
"coinbaseRaw": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f",
"medianFee": 14,
"feeRange": [
1,
1,
2,
14,
15,
20,
347
],
"totalFees": 13804415,
"avgFee": 5287,
"avgFeeRate": 14,
"pool": {
"id": 43,
"name": "SlushPool",
"slug": "slushpool"
},
"matchRate": 100
},
"id": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe",
"height": 736914,
"version": 555696132,
"timestamp": 1652876939,
"bits": 386466234,
"nonce": 3839610443,
"difficulty": 31251101365711.12,
"merkle_root": "dc6d15f641e7af26dbaf3ee37203155f8053a8755e85f4955d11ea0c54008b16",
"tx_count": 2612,
"size": 1450209,
"weight": 3931749,
"previousblockhash": "00000000000000000002b5b2afc1c62e61e53f966b965a9a8ce99112e24066ae"
},
...
]`,
},
codeSampleTestnet: {
esModule: [],
commonJS: [],
curl: ['2226118'],
response: `[]`
},
codeSampleSignet: {
esModule: [],
commonJS: [],
curl: ['88832'],
response: `[]`
curl: `/api/blocks/%{1}/%{2}`,
commonJS: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
`,
esModule: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
console.log(getBlocks);
`,
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: emptyCodeSample,
codeSampleLiquidTestnet: emptyCodeSample,
codeSampleBisq: emptyCodeSample,
codeSampleBisq: {
esModule: ['0', '5'],
commonJS: ['0', '5'],
curl: ['0', '5'],
response: `[
{
"height": 739030,
"time": 1654203258000,
"hash": "000000000000000000036bc04416ddeec264cbb977a9cd9e454897acb547b601",
"previousBlockHash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
"txs": [ ... ],
},
{
"height": 739029,
"time": 1654203236000,
"hash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
"previousBlockHash": "00000000000000000008dd87e9486cd0d71c5d84e452432bab33c2a0cbaa31ce",
"txs": [ ... ],
},
{
"height": 739025,
"time": 1654199569000,
"hash": "000000000000000000021e9ce82dec208af75807f92a9b1d9dae91f2b4d40e24",
"previousBlockHash": "00000000000000000002db644c025a76464b466d25900402452b07213b30c40b",
"txs": [ ... ]
},
{
"height": 739023,
"time": 1654198597000,
"hash": "0000000000000000000702ce10250a46bea4155ca7acb869f3ea92c1e3a68bc5",
"previousBlockHash": "00000000000000000002b3d6c1adc5676262ded84181982f88dbd357b9f9d1ec",
"txs": [ ... ]
},
{
"height": 739020,
"time": 1654197263000,
"hash": "000000000000000000046eb46ad941028381d3534c35658f9c80de0641dbbb31",
"previousBlockHash": "000000000000000000073f1c49b4c4895f3fa6b866d1e21ab8b22f3f9318b42f",
"txs": [ ... ]
}
]`
},
}
}
},
@@ -6070,7 +6107,7 @@ export const faqData = [
showConditions: bitcoinNetworks,
fragment: "looking-up-fee-estimates",
title: "How can I look up fee estimates?",
answer: "<p>See real-time fee estimates on <a href='/'>the main dashboard</a>.</p><p>Low priority is suggested for confirmation within 6 blocks (~1 hour), Medium priority is suggested for confirmation within 3 blocks (~30 minutes), and High priority is suggested for confirmation in the next block (~10 minutes).</p>"
answer: "<p>See real-time fee estimates on <a href='/'>the main dashboard</a>.</p><p>Here is an overview of Mempool's feerate suggestions:</p><ul> <li><b>High Priority.</b> This figure is the median feerate of transactions in the <a href='/mempool-block/0'>first mempool block</a>. Consider using this feerate if you want confirmation as soon as possible.</li><li><b>Medium Priority.</b> This figure is the average of the median feerate of the <a href='/mempool-block/0'>first mempool block</a> and the median feerate of the <a href='/mempool-block/1'>second mempool block</a>.</li><li><b>Low Priority.</b> This figure is the average of the Medium Priority feerate and the median feerate of the <a href='/mempool-block/2'>third mempool block</a>. Consider using this feerate if you want confirmation soon but don't need it particularly quickly.</li><li><b>No Priority.</b> This figure is either 2x the minimum feerate, or the Low Priority feerate (whichever is lower). Consider using this feerate if you are in no rush and don't mind if confirmation takes a while.</li></ul><p>In all cases, the suggested feerate is adjusted lower if any of the mempool blocks involved in the calculation are not full (example: if there is only 1 mempool block that's less than half-full, Mempool will suggest a feerate of 1 sat/vB—not the median feerate of transactions in the block).</p><p>Mempool blocks use feerates, transaction sizes, and other metrics to <b>forecast</b> which transactions will be in future blocks. Actual blocks will turn out to be different: miners have their own views of the mempool, their own algorithms for determining which transactions to include in a block, etc.</p><p>Ultimately, the Bitcoin network is not perfectly predictable, so fee estimation cannot be perfectly precise.</p><p><b>Use Mempool's feerate suggestions as a guide, and understand that they do not guarantee transaction confirmation in any period of time.</b></p>"
},
{
type: "endpoint",

View File

@@ -14,6 +14,7 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
import { GraphsComponent } from '../components/graphs/graphs.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
import { PoolComponent } from '../components/pool/pool.component';
import { TelevisionComponent } from '../components/television/television.component';
@@ -40,6 +41,7 @@ import { CommonModule } from '@angular/common';
BlockFeeRatesGraphComponent,
BlockSizesWeightsGraphComponent,
FeeDistributionGraphComponent,
MempoolBlockOverviewComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
LbtcPegsGraphComponent,

View File

@@ -21,9 +21,11 @@ export interface WebsocketResponse {
loadingIndicators?: ILoadingIndicators;
backendInfo?: IBackendInfo;
da?: DifficultyAdjustment;
fees?: Recommendedfees;
'track-tx'?: string;
'track-address'?: string;
'track-asset'?: string;
'track-mempool-block'?: number;
'watch-mempool'?: boolean;
'track-bisq-market'?: string;
}
@@ -43,6 +45,16 @@ export interface MempoolBlock {
index: number;
}
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
transactions: TransactionStripped[];
}
export interface MempoolBlockDelta {
added: TransactionStripped[],
removed: string[],
}
export interface MempoolInfo {
loaded: boolean; // (boolean) True if the mempool is fully loaded
size: number; // (numeric) Current tx count
@@ -65,3 +77,11 @@ export interface IBackendInfo {
gitCommit: string;
version: string;
}
export interface Recommendedfees {
fastestFee: number;
halfHourFee: number;
hourFee: number;
minimumFee: number;
economyFee: number;
}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
@@ -80,6 +80,8 @@ export class StateService {
bsqPrice$ = new ReplaySubject<number>(1);
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
txReplaced$ = new Subject<ReplacedTransaction>();
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
@@ -90,6 +92,7 @@ export class StateService {
previousRetarget$ = new ReplaySubject<number>(1);
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
live2Chart$ = new Subject<OptimizedMempoolStats>();

View File

@@ -27,6 +27,8 @@ export class WebsocketService {
private lastWant: string | null = null;
private isTrackingTx = false;
private trackingTxId: string;
private isTrackingMempoolBlock = false;
private trackingMempoolBlock: number;
private latestGitCommit = '';
private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number;
@@ -102,6 +104,9 @@ export class WebsocketService {
if (this.isTrackingTx) {
this.startMultiTrackTransaction(this.trackingTxId);
}
if (this.isTrackingMempoolBlock) {
this.startTrackMempoolBlock(this.trackingMempoolBlock);
}
this.stateService.connectionState$.next(2);
}
@@ -157,6 +162,17 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' });
}
startTrackMempoolBlock(block: number) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true
this.trackingMempoolBlock = block
}
stopTrackMempoolBlock() {
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.isTrackingMempoolBlock = false
}
startTrackBisqMarket(market: string) {
this.websocketSubject.next({ 'track-bisq-market': market });
}
@@ -263,6 +279,10 @@ export class WebsocketService {
this.stateService.difficultyAdjustment$.next(response.da);
}
if (response.fees) {
this.stateService.recommendedFees$.next(response.fees);
}
if (response.backendInfo) {
this.stateService.backendInfo$.next(response.backendInfo);
@@ -289,6 +309,16 @@ export class WebsocketService {
});
}
if (response['projected-block-transactions']) {
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions);
} else if (response['projected-block-transactions'].delta) {
this.stateService.mempoolBlockDelta$.next(response['projected-block-transactions'].delta);
}
}
}
if (response['live-2h-chart']) {
this.stateService.live2Chart$.next(response['live-2h-chart']);
}

View File

@@ -71,6 +71,7 @@ import { RewardStatsComponent } from '../components/reward-stats/reward-stats.co
import { DataCyDirective } from '../data-cy.directive';
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component';
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
@NgModule({
declarations: [
@@ -136,6 +137,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
RewardStatsComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
SvgImagesComponent,
],
imports: [
CommonModule,
@@ -228,6 +230,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
RewardStatsComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
SvgImagesComponent,
]
})
export class SharedModule {

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

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